SymbolResolver.java
/*
** Module : SymbolResolver.java
** Abstract : manages the symbol dictionaries and lookup process
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ---------------------------------- Description ----------------------------------
** 001 GES 20041104 @18650 First version, with support for resolving:
** - optionally abbreviated keywords
** - variables with multiple scoping levels
** including a special global scope
** 002 GES 20041115 @18651 Added support for:
** - functions
** - procedures
** - labels
** 003 GES 20041208 @18937 Added new methods for lookup of variables
** and functions. These new methods are now
** using the old "default" names and the old
** methods that handle exact matching are under
** new names lookup*Exact(). The new methods
** implement an algorithm that checks to see
** if a match is made to a language keyword,
** if such a match exists, a 2nd level of
** processing occurs. Otherwise the processing
** is the same as before. This new algorithm
** is necessary to allow built-in functions and
** variables to be properly resolved, since
** these symbols are keywords which must support
** the reserved/unreserved attribute and the
** matching must be done considering
** abbreviations.
** 004 GES 20041217 @19041 Added a first pass implementation for the
** lookup of database objects (databases,
** tables and fields) in both unqualified
** and partially/fully qualified forms. In
** addition, table and field abbreviations are
** fully supported. Warning: table support is
** hard coded at this time.
** 005 NVS 20041225 @19109 Now the label dictionary is scoped, with one
** label per scope (Progress allows only one
** label per block). Modified addLabel creates
** new scope implicitly. Added deleteLabel()
** method which deletes the current scope.
** 006 ECF 20050103 @19188 Reimplemented lookupDatabase, lookupTable,
** and lookupField methods to handle exceptions
** thrown by SchemaDictionary. In each case, if
** an exception is caught, error information is
** written to stdout and -1 is returned as the
** token type.
** 007 GES 20050103 @19192 Added isTable and isField methods which
** don't throw exceptions. This allows the
** parser to test things that may be ambiguous
** without being hit over the head and left
** bleeding in the gutter.
** 008 GES 20050107 @19270 Widgets are handled just like variables
** (they share the same namespace) in Progress.
** However due to the fact that some symbols
** do "double duty" as both variables and as
** widgets, a separate widget namespace is
** kept so that the generic one name to one
** token type mapping can be kept for the
** majority of the standard name lookups. A
** new widgetDict member is added and two
** methods (addWidget and lookupWidget) are
** provided to provide access. This new
** namespace does participate in the scoping
** just like variables.
** 009 GES 20050118 @19336 Added support for modifying the schema
** namespace (adding tables, fields and lists
** of fields).
** 010 GES 20050119 @19346 Added support for record scoping (driving
** the schema's scopes and which tables are
** promoted to the current scope).
** 011 GES 20050127 @19449 Added new namespaces for frames, menus (+
** sub-menus) and menu-items.
** 012 GES 20050128 @19460 Added new namespace for streams.
** 013 GES 20050201 @19513 Added additional parser token type to
** schema dictionary data type conversions
** (for variable types like VAR_INT). Also
** added dump support for the schema dictionary.
** 014 GES 20050204 @19570 Added loadSchemaDatabase() method to load
** an arbitrary non-default database into the
** schema namespace.
** 015 GES 20050207 @19624 Removed deprecated method call to schema
** namespace.
** 016 GES 20050209 @19737 Modified addTable() method to allow user
** control over where a table is created,
** either in the global scope or in an internal
** procedure or function scope (buffers only).
** 017 GES 20050210 @19749 Added the ability to add a variable using
** data contained in a wrapper class. This may
** be extended later to store more data in the
** dictionary than is possible today. In
** addition a new method of adding lists of
** variables, widgets and frames to the
** respective dictionaries was provided. Also
** added a method for adding aliases to the
** schema namespace.
** 018 GES 20050218 @19860 Minor improvements to the loading of
** variables, to allow fields and a special
** token type of DYNAMIC to be loaded by the
** same mechanism.
** 019 GES 20050307 @20217 Added methods to lookup the fully qualified
** name of a field or table.
** 020 GES 20050331 @20561 Major additions to store options at the
** time that variables and temp-tables are
** defined. These options are saved in the
** variable dictionary in the case of vars and
** are pushed out to the schema dictionary in
** the case of temp-tables. All variable and
** temp-table definitions that are LIKE another
** variable, field or table now honor and
** bidirectionally pass the options as needed.
** Variable and field references (the lvalue
** rule) store annotations for non-default
** options. WARNING: INITIAL, LABEL and
** VIEW-AS are 3 common options that are not
** processed or are only partially processed
** at this time.
** 021 ECF 20050331 @20613 Implemented all schema dictionary hooks added
** for 020 (@20561).
** 022 GES 20050401 @20596 Added support for a temporary index to allow
** the linking of a variable reference to its
** original definition at parse time. This is
** needed because project-unique IDs can't be
** easily generated at parse time, but are only
** added after the entire tree is complete.
** 023 GES 20050406 @20651 Massive rewrite to use ASTs to store variable
** options and to exchange ASTs with the schema
** dictionary and fully handle field and
** variable options. In addition it is now
** possible to instantiate this class in a
** manner that bypasses the schema dictionary.
** This allows the ProgressLexer to be used
** without dependencies upon an already
** functional schema.
** 024 ECF 20050413 @20701 Implemented all schema dictionary hooks which
** needed to be modified as a result of 023
** (@20651). TODO: still need to handle VALIDATE
** keyword.
** 025 GES 20050414 @20723 Converted function processing into a wrapped
** approach that allows storing other attributes
** and providing annotation helpers. This
** required some rework of the variable/field
** processing.
** 026 GES 20050509 @21132 Modified annotateVariableOptions() to be
** safe when called by the class
** ProgressExpressionEvaluator which short-
** circuits the normal variable dictionary
** processing and thus can generate nulls
** where they are normally not possible.
** 027 GES 20050822 @22187 Support schema dictionary record type
** lookups for fields and buffers. This
** reports the token type of the backing
** construct (table, temp-table or work-table).
** 028 GES 20050830 @22431 Added attribute/method namespace support.
** 029 GES 20050922 @22778 Support schema dictionary buffer name
** lookups for fields and records. This is
** sometimes (often) different from the fully
** qualified schema name and is needed to know
** which buffer this reference is uniquely
** referencing.
** 030 GES 20051004 @22931 Added database name lookup for tables (this
** is sometimes different from the fully
** qualified schema name).
** 031 GES 20051013 @23058 Moved field annotation here from parser and
** added helper to create a field node based
** on an unambiguous field name.
** 032 GES 20051109 @23282 Added more annotations and tempidx support
** for non-var widgets (buttons/browse) by
** using the wrapper approach.
** 033 ECF 20060425 @25740 Enhanced support for define temp-table. A
** field can be like a specific element of an
** extent field.
** 034 ECF 20060427 @25823 Enhanced support for defining a variable like
** a database field. Enabled specification of a
** specific element of an extent field as the
** model field.
** 035 GES 20070329 @32655 Extended the concept in H034 to all forms of
** variable and field definition using another
** variable.
** 036 GES 20070404 @32793 Pass through a marker when adding a scope to
** the schema dictionary. This allows the new
** scope to be associated with the fact of
** whether the scope belongs to an internal
** procedure, user-defined function or trigger.
** 037 GES 20070418 @33112 Added support for COM-HANDLE types.
** 038 GES 20070528 @33788 Added access to function definition object.
** 039 GES 20070629 @34346 Changed APIs to pass through enough ASTs
** to allow the schema dictionary to set the
** line and column info for temp-table ASTs
** created during parsing.
** 040 GES 20070907 @35010 Support for 10.1B data types.
** 041 GES 20070911 @35138 Support for 10.1B class and package
** namespaces.
** 042 GES 20070923 @35184 Added removeKeyword().
** 043 GES 20070927 @35257 Modified promotion behavior to match new
** interfaces in the schema dictionary. Added
** table promotion when qualified field
** references are encountered.
** 044 GES 20071012 @35529 Added separate namespaces for dataset,
** data-source and data-relation resources.
** 045 GES 20071107 @35900 Made most schema related methods safe to
** use with a null schema dictionary. This
** allows the parser to operate on a wide range
** of expressions without any knowledge or
** dependency upon the schema dictionary. This
** is important in cases of preprocessor
** expression evaluation, where text may need
** to be parsed as a symbol instead of as a
** field name (but if the schema dictionary
** was loaded, the preprocessor symbol would
** resolve as a field which is an invalid
** result).
** 046 GES 20071129 @36187 Removed support for DYNAMIC hints (no longer
** needed).
** 047 GES 20071207 @36286 Signature changes to pass more data to the
** schema dictionary. Also added a global flag
** to enable/disable table promotion.
** 048 ECF 20080704 @39145 Minor memory optimization. Use Boolean
** constants instead of instantiating new
** Boolean objects.
** 049 GES 20090429 @42065 Match package and class name changes.
** 050 GES 20110712 Made failure to find a class abend more gracefully
** (and provide more useful data).
** 051 GES 20110811 Added simple list of handles that could be possible
** active-x targets. This should probably be a real
** scoped dictionary, but for a start it should handle
** virtually all cases.
** 052 GES 20110822 Added support for loading built-in 4GL OO classes/
** interfaces and .NET classes/interfaces. Added
** static support. Added support for define event
** when loading a class via AST. Events are treated
** like properties and vars (they share the same
** namespace). Reworked class loading for prescanning
** and to improve performance.
** 053 GES 20111005 Added safety code to avoid class loading issues
** when the OO skeleton class directories don't exist.
** 054 CA 20130225 Annotate the fields with the db name.
** 055 CA 20130307 Fixed abbreviated field name disambiguation - the tables which are
** weak reference in the current scope have precedence over others.
** 056 CA 20130307 When multiple there are multiple definitions for the same var, only
** the first one has its tempidx set and is saved in the dictionary.
** 057 CA 20130307 Fixed honoring of VALIDATE clause for temp-table fields defined using
** the LIKE clause and tables defined using the LIKE clause.
** 058 GES 20130317 Added support for the USING clause when loading from an AST.
** 059 CA 20131029 All the static info is kept context-local, for runtime query
** conversion support.
** 060 OM 20131205 Fixed addLocalVariableLike() by creating the new variable with exact
** type as the variable passed as parameter.
** 061 GES 20140129 Fixed an exists() use case (this is also an optimization).
** 062 GES 20140428 Added tryLoadClass() and differentiated the builtin annotation for
** classes as builtin-cls. Fixed OO lookup logic to match changes in
** ClassDefinition processing. Added methods to more properly support
** the keyword ignore list.
** 063 GES 20141125 Added caching functions to allow local vars to be restored in
** function defs that use the 4GL feature of making their parms list
** optional when a forward function def has already specified the
** parms.
** 064 VIG 20141104 Added menu support.
** 065 ECF 20151030 Added methods to retrieve schema ASTs for database tables and
** fields, and to manage field widget references by frame.
** 066 GES 20160224 Implemented a configuration parameter to allow overriding the
** default location for skeleton classes. Complete rewrite of schema
** dictionary resolution to handle the dynamic lookup in the class
** hierarchy of protected buffers and temp-tables. Added support for
** query resources. Queries, datasets and data-sources are now
** looked up in the class hierarchy too.
** 067 ECF 20170303 Honor FOR TEMP-TABLE clause in DEFINE BUFFER/PARAMETER statements.
** 068 CA 20170523 Fix parsing of implicit Progress.Lang.Object super-class inheritance.
** 069 GES 20170706 Added utility functions used to generate the class map.
** GES 20170708 When not in silent mode, will print the class name of any
** recursively parsed class file.
** CA 20170711 1st level OO class scanning is decoupled from full parse: first, all
** references from members or inheritance are resolved, for the entire
** graph, and only after that the reached classes are parsed.
** CA 20170721 Remove all referenced classes/interfaces loaded during a 1st level
** scan, if it fails when loading a class.
** CA 20170724 When pre-parsing a class, failures to resolve a class starting with
** the second or subsequent parsing level will result in 'sink-ing' all
** references to the missing class, and not fail. Failures will be
** emitted only if the currently parsed class has a direct reference to
** a missing class. This is to emulate the 4GL compiler, which doesn't
** resolve the entire class reference graph, but only the first level;
** the compromise is needed because FWD requires to resolve all class
** references (members and types) at parsing time.
** CA 20170725 When processing the method block during first level parse, try to
** resolve the expected type of the data member from the currently
** processed class first.
** CA 20170804 processHierarchy first checks the real class hierarchy if
** ignoreOORefs is set.
** 070 OM 20170802 Dropped possible ActiveX detection. Removed redundant casts.
** 071 CA 20170825 Initial support for USE-DICT-EXPS: not fully complete for the HELP
** behaviour.
** 072 CA 20171010 The key for a frame field AST is schemaname$bufname, and it must be
** used to collect all unique frame fields in addFramefields.
** Fixed convertSeparator() when used from Windows OS.
** 073 GES 20171017 Added hexadecimal literal support.
** 074 GES 20180303 Fixed javadoc.
** HC 20180312 Fixed lookup of STATIC flag of class data members and methods. The
** lookup didn't work correctly during class prescan mode.
** CA 20180316 MOCK_CLASS_DEF lookupMethod and lookVariable APIs will check for a
** defined var: if so, it will return -1, as it doesn't match a class
** member, but only if localLookup flag is set.
** HC 20180319 Fixed an NPE when a referenced class is not defined (i.e. the class file not
** found). In this case compiler error must be issued.
** Fixed resolution of static class members.
** HC 20180320 Added support for referencing .NET inner classes (i.e. namespace.Outer+Inner).
** CA 20180404 CAN-FIND allows field abbreviations to be resolved even if ambiguous:
** it will return the first field which matches the prefix.
** DEF BUFFER b FOR TEMP-TABLE t. may target either a temp-table or a
** physical table (temp-table has precedence).
** 075 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 20180412 Do not fail if we can't parse a class which is reached in
** pre-scan-mode, to mimic the 4GL's lenient compile behaviour, where
** class references in i.e. OO method bodies are resolved and compiled
** at runtime, and not via COMPILE statement.
** Fixed resolveClassName when classes are (partially) qualified and
** also to address missing classes, at callgraph build.
** GES 20180415 Changed signatures to allow passing a sequential flag. The rest of
** CREATE-LIKE-SEQUENTIAL is left as a TODO.
** GES 20180425 Made failures generate a warning in addIndexFrom() instead of being
** fatal, since the 4GL allows garbage to be specified (and silently
** accepts it).
** ECF 20180422 Removed class-level synchronization and static initialization of
** context-local resource.
** CA 20180510 Allow the table options to be emitted to the associated .dict file.
** 076 CA 20180621 Added promoteAllTables.
** 077 CA 20181029 Reworked some APIs related to registration of attribute/methods.
** 078 CA 20180927 annotateClassRef should try to load the class, if not found.
** Package names with '.*' support are not reported as abend, if they
** do not exist.
** GES 20181030 Added inherits clause support for interfaces.
** GES 20181031 Ignore USING stmts that have duplicate base names.
** GES 20181106 Mark built-in global variables with a builtin annotation so they can
** be distinguished later. Simplified addDataMember() and fixed the
** default access mode of define variables to PRIVATE.
** CA 20181113 lookupDataMember will look for the member in the currently parsed
** class def - if not found, will default to the 'mock def'. This is
** useful only in prescan mode, when we don't have the entire class
** parsed.
** GES 20181117 Reworked the pre scan class processing to use a cut down parser
** which eliminates the need for the ignoreOORefs approach and the
** related patches. These have all been removed.
** CA 20181120 Removed the class_map.xml approach, as this does not allow multiple
** classes (with the same qualified name) to exist in different source
** folders - PROPATH is used instead.
** Interfaces will allow access to the Progress.Lang.Object methods.
** CA 20181121 System.Object is the root class for all .NET classes.
** Un-indexed .NET-style array variable references must act as
** System.Array.
** GES 20181122 Fixed the association of data members with the chained instance from
** which it was parsed.
** CA 20181123 lookupWrapped - if the text string is not a built-in var or function
** name, then fallback to looking for a variable - as 4GL allows
** definitions of OO data members having a reserved keyword like 'Back'.
** 079 GES 20181129 Associate the full filename with any new class or interface def.
** 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.
** Annotate the USING class reference with the class info (and prescan
** if needed).
** GES 20181216 Added method_def tempIdx support.
** 080 CA 20190219 tempIdx support is added to constructors, too - so that NEW can
** reference the exact definition via the refid annotation.
** OM 20190322 Fixed relative path of mapped classes.
** 081 OM 20190415 Added DataSet and DataSource support.
** 082 CA 20190423 Missing OO 4GL imports are not emitted.
** Misc OO dismabiguation fixes, fixed 'var leak' during pre-scan and
** tempidx collisions from pre-scan and normal parsing (tempidx for OO
** is maintained at the ClassDefinition).
** 083 CA 20190520 Solved converted method names for override and overload cases.
** The 'provisional' members are required to be resolved once the
** initial pre-scan of a class has finished, by doing a full-parse on it
** and any super-classes or interfaces. This is required because at a
** time a class is referenced, it may not have been by FWD's normal
** processing, and we still need the provisional members solved.
** 084 CA 20190604 Fixed a typo for LENGTH-OF name.
** 085 SVL 20190614 Support for LIKE-SEQUENTIAL.
** 086 ECF 20190620 EmptyIterator API change.
** 087 OM 20190611 Improved support for before and after temp-tables.
** 088 GES 20190518 Added strong scope marking for the schema dictionary.
** 20190619 Added multiple inheritance (only needed for interfaces).
** 20190620 Added enum support.
** 20190622 Fixed processHierarchy() which was broken with multiple inheritance
** changes.
** 20190624 Expanded standard class reference annotations in annotateClassRef()
** and fixed tempIdx processing for enum defs.
** 20190625 Removed previous .NET dependency tracking approach.
** 20190710 Fixed dump of .NET dependency report.
** ECF 20190717 Qualify table names with database name in promoteAllTables.
** RFB 20190912 Added a new constructor which allows SymbolResolver to bypass
** initializing symbol dictionaries. This increases efficiency.
** Currently relex in schema.g is the only place using this.
** RFB 20190913 Added new method srchPropathFindFile which handles partially
** qualified class names by prepending the PROPATH entries before
** calling the original findFile.
** RFB 20190913 Enabled check in loadClass for preScanRefs to prevent recursive
** death. Similarly, in addPackage, we prevent a call to
** annotateClassRef if classes are "using" each other
** GES 20190823 Added support for direct Java OO references/usage from 4GL code.
** RFB 20191008 translateType receives null parm when there isn't any need to dive
** deeper to determine the data type. We need to protect the jclass
** check if null is passed.
** GES 20191010 Added dbtype() builtin function for expression evaluator use.
** GES 20191012 Check for LBRACKET after finding the correct node of the sub-expr
** in readExtent().
** GES 20191015 Removed debugging code.
** 089 RFB 20200214 Allow for "qualified" annotation in pre-scan, since there are several
** downstream scenarios when it's depended up for knowing the class name.
** One warning is that we don't know whether it's a Java or Progress
** class, so we are assuming it's Progress, and converting to lowercase.
** In calculateParents we are giving up the ghost if this annotation is
** not found, since it would lead to NPE.
** Also made some log message formatting changes, with respect to the
** recursion during pre-scan.
** CA 20200219 Fixed class type (OE, Java, .NET) resolution in resolveClassName.
** GES 20200301 Added NameNode support to some field-level lookups.
** GES 20200302 Changes to use findFieldInfo() and handle null returns.
** GES 20200108 Added support for direct usage of fully qualified Java class names
** without requiring a USING statement first.
** CA 20200304 Added support for USING com.foo.Bar FROM JAVA (qualified class name).
** 090 CA 20200412 Added incremental conversion support.
** 091 CA 20200503 Added support for accumulators referenced by their default variables,
** like TOTAL, COUNT. This requires tracking the inner block scopes, and
** these accum builtin variables has precedence over an outer defined
** variable.
** Lookup for chained object members must always be done at the class
** members, and not local variables.
** GES 20200522 Removed class name map file name (dead code).
** CA 20200529 Fixed an issue for incremental conversion, when the AST for a skeleton class
** (which was parsed in a previous conversion) does not exist.
** 092 CA 20200718 ConversionData must not be used in runtime conversion mode.
** 093 IAS 20200624 Added checks for <code>null</code>
** 094 CA 20200804 Emit bulk setter and getter for an extent property.
** CA 20200930 Use an exemplar instance to pre-populate the dictionaries at runtime conversion.
** CA 20210113 If a implemented skeleton's Java method name collides with one part of
** NameConverter.isInternalMethodName's, fail immediately, as this means the
** implemented skeleton method's Java name is not following NameConverter's rules.
** CA 20210203 Fixed conversion issues with OUTPUT/INPUT-OUTPUT for extent or scalar parameters,
** involved in dynamic or static OO calls, functions or procedure calls. This
** includes mostly cases for OO properties/variables (static and non-static).
** OM 20210219 lookupWorkerExact returns -1 if null dictionary is provided instead of NPE.
** GES 20210610 Enabled lookup of .NET inner class names (and their associated filenames) which
** have an embedded '+' character between the containing class and the inner class
** name. For example, the qualified name some.package.ContainingClass+InnerClass
** and its matching filename some/package/ContainingClass+InnerClass.cls are now
** supported cases. Changed how variable instances are created. Added support for
** the undocumented .NET enum value__ member.
** GES 20210715 Added loadConvertedClassByQname().
** CA 20210920 Added NPE protection for some registries, for runtime conversion.
** HC 20211001 Implementation of i18n support.
** OM 20211122 Added hints-based support for 'unloading' schema.
** CA 20220113 Widget lookup must not be performed (and return 'no match') if the closest
** definition is a variable which can't be a widget (HANDLE, object, etc).
** CA 20220128 Added an abend protection and log for annotateObjectMethod(), seen in incremental
** conversion, but with no local duplicate.
** ME 20211026 Added support for POLY data type.
** GES 20220309 Accumulator variables must be propagated to the parent/containing block. In the
** 4GL such implicit variables outlive their inner block scope.
** RFB 20220325 preferDefaultBuffer is passed through annotateField and lookupFieldInfo to
** findFieldInfo so the default buffer can be considered. Ref #4791.
** GES 20220404 Reduced excessive logging.
** TJD 20220504 Java 11 compatibility minor changes
** CA 20220408 Ensure that the class hierarchy is reloaded properly when initial loading
** was due to references in a procedure.
** Fixed 'translateType' for TABLE parameters.
** BUFFER definitions are tracked only in preScan or if they are not defined in a
** method.
** Annotate THIS-OBJECT and SUPER constructor call parameters.
** CA 20220429 Abort parsing if the parent class can't be found in 'parseHierarchy'.
** CA 20220513 Added isBuiltinFunction(String).
** CA 20220516 Fixed tracking of extent data members (variables or properties).
** Indexed extent property setters have the index as int64 (and this is forced for
** explicit indexed setters, too).
** GES 20220502 Added lookupTableInfo() to reduce the number of name node lookups. Reworked
** translateType() to properly handle buffers, tables, datasets, table-handles,
** dataset-handles and objects. Reworked method name processing and how we track
** methods so that converted method name conflicts are resolved within the class
** hierarchy. This minimizes the number of conflicts since previously the
** disambiguation was done at a project level.
** CA 20220531 During prescan, always track the skeleton OO property or events.
** Always track the default constructor, as it is not mandatory to be defined in the
* class. Progress.Lang.Object must be loaded before any enum is loaded, otherwise
** there will be circular references. OVERRIDE option at PROPERTY or METHOD is not
** mandatory when overriding an interface method/property.
** Fixed misc issues related to calculating the call signature or type translation.
** 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 20220818 An abstract property can define only a getter or a setter, while the implementation
** can define both. For this reason, only the overridden accessor must have the
** @Override annotation.
** TJD 20230220 Implement isExactMatchField to allow checking exact field names
** 095 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 096 OM 20230115 Replaced absolutePath(), relativePath(), upPath() and downPath() with faster
** versions, based on node types rather on string paths.
** 097 CA 202302426 For method arguments, mark those which are from an expression (i.e. using
** operators).
** 098 CA 20231004 Fixed conversion for class events with parameters of type DATASET, TABLE,
** DATASET-HANDE, TABLE-HANDLE and BUFFER.
** 099 CA 20230929 Method chain calls starting with SUPER keyword can not be morphed to dynamic
** invoke, as the super calls require JVM to handle them.
** CA 20231007 Arguments for direct method calls which can't be emitted as direct java calls
** (as they are considered 'dynamic poly') must be wrapped in a 'polyArg' to check
** the type.
** 100 CA 20231019 Show linkage errors when resolving Java class names.
** 101 OM 20240305 Added [word-indexed] annotation on word indexed fields.
** 102 OM 20240606 Fixed [word-indexed] for dynamic case. Local optimisations.
** 103 GBB 20240826 Moving FileSystemDaemon to osresource package.
** 104 CA 20241026 Fixed .NET arrays property accessed via [].
** Fixed an issue with circular references when parsing classes: if a parent is
** already 'pre-scanned', avoid parsing the class and let it parse when it's reached
** again.
** The parent of .NET enums is System.Enum.
** The '+' in .NET inner class names must be replaced with File.separatorChar.
** Class hierarchy in 'processed' must be calculated when the parse finishes for the
** file, and not when 'tryLoadClass' is called.
** CA 20241104 In 'tryLoadClass', force throwing an exception if the class can't be loaded.
** AOG 20241111 Changed addDataMember to look directly in varDict for class member.
** CA 20250225 'annotateNumericLiteral' must move a suffix '+' or '-' to the beginning, so it can
** be parsed.
** 105 CA 20250319 An worker can be used before the 'qualified_oo_name' is set in annotation
** (like in schema conversion phase), so ensure we can load the ClassDefinition.
** CA 20250319 A provisional class var found during pre-scan, which is referenced before the
** definition statement and is referenced via a chain, must be resolved even if is
** provisional - this is the only case allowed in 4GL, when a var can be defined
** after it is referenced.
** 106 ES 20250327 Verify if a class definition is present in the conversion list, not just propath. And
** add it automatically if it is not present in conversion list.
** ES 20250328 Added additional check to verify whether a file is present in the conversion list,
** overrides a 4gl build in class. In such case remove it from conversion.
** Modify the propath entries to store a list on sources.
** 107 ES 20250422 Check class definiton before adding to the conversion list.
** 108 CA 20250422 Normalize the file names when calculating the class list in 'getClassFileList'
** and 'createClassMappings', so all are relative to project home.
** 109 ICP 20250407 Added getLegacyPackageName method that extracts the legacy package name from
** the fully qualified class name.
** Corrected calcAccessMode to also calculate the package private access mode.
** Modified readExtent to resolve the extent in the case the node's type is
** EXPRESSION and the extent annotation is present in the child.
** 110 CA 20231215 Protect against infinite recursion in loadClassDefinition.
*/
/*
** 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.uast;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.Stack;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.logging.*;
import com.goldencode.ast.*;
import com.goldencode.util.*;
import antlr.*;
import com.goldencode.p2j.cfg.Configuration;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.convert.db.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.*;
// ambiguous reference
import com.goldencode.p2j.util.osresource.FileSystemDaemon;
import org.reflections.*;
/**
* Contains a dictionary for each type of language namespace and provides
* methods to maintain and lookup in the respective namespaces.
* <p>
* Provides support for the following namespaces:
* <ul>
* <li> language keywords (optionally abbreviated)
* <li> global and local variables with multiple arbitrary levels of
* scoping
* <li> functions (both built-in and user-defined)
* <li> procedures
* <li> classes
* <li> package names
* <li> labels
* <li> schema names
* <ul>
* <li> databases
* <li> tables - qualified or not, optionally abbreviated
* <li> fields - qualified or not, optionally abbreviated
* </ul>
* <li> widgets (this namespace is artificially separated from the
* variable namespace though normally this is not the case in
* Progress)
* <li> frames
* <li> menus and sub-menus
* <li> menu-items
* <li> streams
* <li> DataSet support
* <ul>
* <li> datasets
* <li> data-sources
* <li> data-relations
* </ul>
* <li> object-oriented support
* <ul>
* <li> packages
* <li> class definitions
* </ul>
* </ul>
* <p>
* All namespaces (except language keywords) provide a mapping between a
* symbol and an integer token type as defined by the Lexer and Parser. It
* is this token type that is returned from most lookup operations.
* <p>
* This class is aware of the parser token types and where necessary it
* converts other values into the proper types for use in the parser.
* At this time this feature is only needed for schema name lookups as
* all other types are already normalized properly in the respective
* dictionary. See {@link #lookupField}.
* <p>
* Only the variable, widget and label namespaces utilize multiple scoping
* levels at this time. All other namespaces are flat.
*/
public class SymbolResolver
implements ProgressParserTokenTypes
{
/** Possible access modes for object data members. */
private static final Set<Integer> ACCESS_MODES = new HashSet<>();
/** Available menu children types. */
private static final Set<Integer> MENU_CHILDREN_TYPES = new HashSet<>();
/** Available sub-menu children types. */
private static final Set<Integer> SUBMENU_CHILDREN_TYPES = new HashSet<>();
/** Class or interface definition file extension. */
public static final String CLASS_EXT = ".cls";
/** Class or interface definition file search specification. */
public static final String CLASS_SPLAT = "*" + CLASS_EXT;
/** Default parent of all OE classes if no explicit parent is specified. */
public static final String ROOT_OBJ_NAME = "Progress.Lang.Object";
/** Default parent of all .NET classes if no explicit parent is specified. */
public static final String ROOT_DOTNET_OBJ_NAME = "System.Object";
/** Default parent of all classes if no explicit parent is specified. */
public static final String ROOT_CLASS_NAME = "Progress.Lang.Class";
/** Relative location for the non-project class/interface 4GL code. */
public static final String SKELETON_PATH = "./p2j/skeleton/";
/** Relative location for the 4GL object-oriented base definitions. */
public static final String OO4GL_PATH = "oo4gl/";
/** Relative location for the .NET base definitions. */
public static final String DOTNET_PATH = "dotnet/";
/** Relative location for project-specific .NET definitions. */
public static final String ASSEMBLY_PATH = "./assemblies/";
/** Relative location for the non-project class/interface 4GL code. */
public static String skeletonPath = SKELETON_PATH;
/** Relative location for the 4GL object-oriented base definitions. */
public static String oo4glPath = SKELETON_PATH + OO4GL_PATH;
/** Relative location for the .NET base definitions. */
public static String dotnetPath = SKELETON_PATH + DOTNET_PATH;
/** Logger */
private static final ConversionStatus LOG = ConversionStatus.get(SymbolResolver.class);
/** Context-local data. */
private static final ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
{
protected WorkArea initialValue()
{
return new WorkArea();
}
};
/**
* Mock class to be able to parse OO references in pre-scan mode, without affecting the lookup,
* as not all classes might be loaded.
*/
private final ClassDefinition MOCK_CLASS_DEF =
new ClassDefinition(null, null, null, false, OOType.CLASS, false, "")
{
@Override
public void annotateMethodCall(String mname,
ParameterKey[] sig,
int access,
boolean isStatic,
boolean isSuper,
boolean internal,
Aast node)
{
// no-op
}
@Override
public String getName()
{
return "MOCK_FWD_CLASS";
};
@Override
public java.util.Set<String> getInterfaces()
{
return Collections.emptySet();
};
@Override
public SchemaDictionary getSchemaDict()
{
return null;
};
@Override
public void addDataSet(Variable var, Aast node, String name, int type, int access, boolean isStatic, boolean inMethod)
{
// no-op
};
@Override
public void addDataSource(Variable var, Aast node, String name, int type, int access, boolean isStatic, boolean inMethod)
{
// no-op
};
@Override
public void addMethod(String name,
int type,
int access,
boolean isStatic,
Aast node,
Aast ret)
{
// no-op
};
@Override
public void addQuery(String name, Aast node, int type, int access, boolean isStatic,
boolean inMethod)
{
// no-op
};
@Override
public void addTable(Aast node, String name, int type, int access, boolean isStatic,
SchemaDictionary schemaDict, boolean inMethod)
{
// no-op
};
@Override
public void addVariable(int type, String name, Variable var, Aast node, int access, boolean isStatic,
String qname, boolean provisional)
{
// no-op
}
@Override
public int lookupDataSet(String name, int access, boolean isStatic)
{
return DATA_SET;
}
@Override
public int lookupDataSource(String name, int access, boolean isStatic)
{
return DATA_SOURCE;
};
@Override
public int guessMethodType(String name, int access, boolean isStatic, boolean internal)
{
if (!currentCls.isEmpty())
{
int type = currentCls.peek().guessMethodType(name, access, isStatic, internal);
if (type != -1)
{
return type;
}
}
int type = localLookup ? SymbolResolver.this.lookupVariable(name) : -1;
return type != -1 ? -1 : OO_METH_CLASS;
};
@Override
public int lookupQuery(String name, int access, boolean isStatic)
{
return QUERY;
};
@Override
public int lookupTable(String name, int access, boolean isStatic)
{
return TABLE;
};
@Override
public int lookupVariable(String name, int access, boolean isStatic)
{
if (!currentCls.isEmpty())
{
int type = currentCls.peek().lookupVariable(name, access, isStatic);
if (type != -1)
{
return type;
}
}
int type = localLookup ? SymbolResolver.this.lookupVariable(name) : -1;
return type != -1 ? -1 : VAR_CLASS;
};
@Override
public String lookupVariableClassName(String name, int access, boolean isStatic)
{
if (!currentCls.isEmpty())
{
String clsName = currentCls.peek().lookupVariableClassName(name, access, isStatic);
if (clsName != null)
{
return clsName;
}
}
return "MOCK_FWD_VAR";
};
/**
* The method indicates whether this inctance represents a "mock" class definition
* used to resolve symbols during class pre-scan mode in case the class file has not been
* parsed.
*
* @return <code>true</code> is this instance represents a "mock" class definition,
* <code>false</code> otherwise.
*/
@Override
public boolean isMock()
{
return true;
}
};
/** Cache of resolved classes, per absolute directory path. */
private static final Map<String, String[]> DIR_CLASSES = new ConcurrentHashMap<>();
/**
* An exemplar from which the dictionaries will be populated, when creating instances via
* {@link #newRuntimeInstance()}.
*/
protected static final SymbolResolver SYMBOL_RESOLVER_EXEMPLAR;
static
{
// the following needs to be done only for runtime conversion
SymbolResolver sym = null;
if (Configuration.isRuntimeConfig())
{
sym = new SymbolResolver()
{
@Override
protected boolean isRuntimeConfig()
{
return false;
}
};
StringReader in = new StringReader("");
ProgressLexer lexer = new ProgressLexer(in, sym);
ProgressParser parser = new ProgressParser(lexer, sym);
}
SYMBOL_RESOLVER_EXEMPLAR = sym;
}
/** Classes in the propath, mapped qualified classname to filename. */
private Map<String, List<String>> propathCls = null;
/** Stores class and interface definitions. */
private ScopedSymbolDictionary classDict = null;
/** Flag indicating if the data member or method is looked up without a qualifier. */
private boolean localLookup = false;
/** Flag indicating that variable lookup must be done only at class members. */
private boolean classLookup = false;
/** Stores keywords and handles abbreviation support. */
private KeywordDictionary kwDict = null;
/** Stores accum variables. */
private ScopedSymbolDictionary accumDict = null;
/** Stores variables, both local (scoped) and global. */
private ScopedSymbolDictionary varDict = null;
/** Cache of local variables used to duplicate function signatures. */
private Map<String, Set<Map.Entry>> varDefCache = null;
/** Stores function names and return types. */
private ScopedSymbolDictionary funcDict = null;
/** Stores procedure names. */
private ScopedSymbolDictionary procDict = null;
/** Stores explicit package names. */
private Map<String, UsingSpec> explPkgDict = null;
/** Stores package search specifications. */
private Set<UsingSpec> searchPkgDict = null;
/** Stores label names. */
private ScopedSymbolDictionary labelDict = null;
/** Stores database object names (databases, tables and fields). */
private SchemaDictionary schemaDict = null;
/** Stores widget names. */
private ScopedSymbolDictionary widgetDict = null;
/** Stores frame names. */
private ScopedSymbolDictionary frameDict = null;
/** Stores sets of database field widget ASTs, organized by their frame names */
private ScopedSymbolDictionary<Set<Aast>> fieldWidgetDict = null;
/** Stores the state of USE-DICT-EXPS flag for each frame. */
private ScopedSymbolDictionary<Boolean> useDictExps = null;
/** Stores menu and sub-menu names. */
private ScopedSymbolDictionary menuDict = null;
/** Stores menu-item names. */
private ScopedSymbolDictionary menuItemDict = null;
/** Stores stream names. */
private ScopedSymbolDictionary streamDict = null;
/** Stores query names. */
private ScopedSymbolDictionary qryDict = null;
/** Stores dataset names. */
private ScopedSymbolDictionary dsetDict = null;
/** Stores data-source names. */
private ScopedSymbolDictionary dsrcDict = null;
/** Stores data-relation names. */
private ScopedSymbolDictionary drelDict = null;
/** Maps keyword token types to the correct attribute and method type. */
private Map<Integer, Integer> attributes = null;
/** Provides access to ATTR_CLASS or METH_CLASS class names. */
private Map<Integer, String> attrTypes = null;
/** Current class or interface definition that is being created. */
private Stack<ClassDefinition> currentCls = null;
/** AST-specific variable definition index workaround. */
private int tempIdx = 0;
/** Current PROPATH. */
private String[] propath = null;
/** The legacy system's filesystem case-sensitivity. */
private boolean caseSens = true;
/** Used for recursive class or interface loading. */
private AstGenerator generator = null;
/** Flag indicating whether silent mode is required. */
private boolean silent = false;
/** Flag to globally enable/disable table promotion. */
private boolean allow = true;
/**
* Flag indicating that an unique index was found in the current query - every subsequent
* BY clause will be allowed to match garbage.
*/
private boolean foundUniqueIndex = false;
/**
* Flag indicating that unique index matching using the BY clause's fields is possible; set via
* {@link #startIndexFieldSearch()}, from {@link ProgressParser#each_first_last_spec}. It will
* be reset via {@link #endIndexFieldSearch()}, at the end of the current query or if a matching
* is not possible (i.e. multiple tables, BREAK clause, not a prefix match for an unique
* index).
*/
private boolean indexFieldSearch = false;
/**
* Flag indicating that we can use the next field reference as an attempted match against an
* index. Set at the beginning of {@link ProgressParser#sort_order_clause()} and reset at the
* end, via {@link #useIndexFieldSearch}.
*/
private boolean useIndexFieldSearch = false;
/**
* The current query's single table, which will be used to disambiguate unqualified fields in
* BY clauses.
*/
private String indexTable = null;
/**
* The current query's single buffer, which will be used to disambiguate unqualified fields in
* BY clauses.
*/
private String indexBuffer = null;
/**
* The currently {@link #collectSortField(Aast) collected AST fields} from BY clauses. All
* these match as a prefix an unique index.
*/
private List<Aast> sortFields = null;
/**
* The longest unique indexes (without any smaller unique index which can be used as a prefix)
* in the {@link #indexTable}.
*/
private Map<Aast, List<Aast>> indexes = null;
/** Are we parsing code inside a method definition (includes constructors, destructors...). */
private boolean inMethod = false;
/** Flag indicating we are in a chained reference. */
private boolean chainedReference = false;
static
{
ACCESS_MODES.add(KW_PUBLIC);
ACCESS_MODES.add(KW_PROTECTD);
ACCESS_MODES.add(KW_PRIVATE);
MENU_CHILDREN_TYPES.add(KW_NEW);
MENU_CHILDREN_TYPES.add(KW_SHARED);
MENU_CHILDREN_TYPES.add(KW_FGCOLOR);
MENU_CHILDREN_TYPES.add(KW_BGCOLOR);
MENU_CHILDREN_TYPES.add(KW_DCOLOR);
MENU_CHILDREN_TYPES.add(KW_PFCOLOR);
MENU_CHILDREN_TYPES.add(KW_FONT);
MENU_CHILDREN_TYPES.add(KW_TITLE);
MENU_CHILDREN_TYPES.add(KW_MENU_BAR);
MENU_CHILDREN_TYPES.add(KW_MENU_ITM);
MENU_CHILDREN_TYPES.add(KW_SUB_MENU);
MENU_CHILDREN_TYPES.add(KW_SKIP);
MENU_CHILDREN_TYPES.add(KW_RULE);
SUBMENU_CHILDREN_TYPES.add(KW_FGCOLOR);
SUBMENU_CHILDREN_TYPES.add(KW_BGCOLOR);
SUBMENU_CHILDREN_TYPES.add(KW_DCOLOR);
SUBMENU_CHILDREN_TYPES.add(KW_PFCOLOR);
SUBMENU_CHILDREN_TYPES.add(KW_FONT);
SUBMENU_CHILDREN_TYPES.add(KW_MENU_ITM);
SUBMENU_CHILDREN_TYPES.add(KW_SUB_MENU);
SUBMENU_CHILDREN_TYPES.add(KW_SKIP);
SUBMENU_CHILDREN_TYPES.add(KW_RULE);
}
/**
* Default constructor that creates an instance with schema lookups
* <b>disabled</b>. Each namespace is initialized.
*/
public SymbolResolver()
{
this(false, true, null, true, true, null);
}
/**
* Constructs an instance with control over disabling schema lookups.
* This allows an instance to be created in the absence of any schema
* support. Each namespace is initialized.
*
* @param schema
* <code>true</code> enables schema dictionary support.
*/
public SymbolResolver(boolean schema)
{
this(schema, true, null, true, true, null);
}
/**
* Constructs an instance with control over disabling schema lookups and
* symbol dictionary. This allows an instance to be created in the absence
* of any schema support and namespace is initialization
*
* @param schema
* <code>true</code> enables schema dictionary support.
* @param caseSens
* <code>true</code> if the legacy system had a case-sensitive file-system.
* @param propath
* The current <code>PROPATH</code> to honor or <code>null</code> for the default
* of ".".
* @param silent
* Flag indicating if <code>Sytsem.out</code> output should be suppressed.
*/
public SymbolResolver(boolean schema, boolean caseSens, String[] propath, boolean silent)
{
this(schema, caseSens, propath, silent, true, null);
}
/**
* Constructs an instance with control over disabling schema lookups. This allows an instance
* to be created in the absence of any schema support. Each namespace is initialized.
*
* @param schema
* {@code true} enables schema dictionary support.
* @param caseSens
* {@code true} if the legacy system had a case-sensitive file-system.
* @param propath
* The current {@code PROPATH} to honor or {@code null} for the default of {@code "."}.
* @param silent
* Flag indicating if {@code System.out} output should be suppressed.
* @param dict
* Flag indicating if symbol dictionary creations should proceed.
* @param bannedSchemas
* The set of banned schemas. The {@code Namespace} will not recognize element from these schemas
* even is they are loaded.
*/
public SymbolResolver(boolean schema,
boolean caseSens,
String[] propath,
boolean silent,
boolean dict,
Set<String> bannedSchemas)
{
// needed for minimal lexer use
kwDict = new KeywordDictionary();
// needed for minimal expression evaluation usage
varDict = new ScopedSymbolDictionary<>(null, false);
funcDict = new ScopedSymbolDictionary<>(null, false);
// dictionaries that are only needed for full 4GL parsing
if (dict)
{
accumDict = new ScopedSymbolDictionary<>(null, false);
varDefCache = new HashMap<>();
procDict = new ScopedSymbolDictionary<>(null, false);
explPkgDict = new LinkedHashMap<>();
searchPkgDict = new LinkedHashSet<>();
labelDict = new ScopedSymbolDictionary<>(null, false);
widgetDict = new ScopedSymbolDictionary<>(null, false);
frameDict = new ScopedSymbolDictionary<>(null, false);
fieldWidgetDict = new ScopedSymbolDictionary<>(null, false);
useDictExps = new ScopedSymbolDictionary<>(null, false);
menuDict = new ScopedSymbolDictionary<>(null, false);
menuItemDict = new ScopedSymbolDictionary<>(null, false);
streamDict = new ScopedSymbolDictionary<>(null, false);
qryDict = new ScopedSymbolDictionary<>(null, false);
dsetDict = new ScopedSymbolDictionary<>(null, false);
dsrcDict = new ScopedSymbolDictionary<>(null, false);
drelDict = new ScopedSymbolDictionary<>(null, false);
attributes = new HashMap<>();
attrTypes = new HashMap<>();
currentCls = new Stack<>();
}
if (schema)
{
// this load is dependent upon a configuration file being available
try
{
schemaDict = new SchemaDictionary(bannedSchemas);
// add a default scope that represents the scope of the external procedure
addSchemaScope(false);
}
catch (SchemaException ex)
{
LOG.log(Level.WARNING,"The schema dictionary is invalid, however all other types of lookups " +
"will work fine --> if there is no schema name processing, then consuming this exception is" +
" safe; if there is schema name processing, then get ready for a null pointer exception!", ex);
}
}
setPropath(propath);
this.caseSens = caseSens;
this.silent = silent;
this.propathCls = new HashMap<>();
if (dict)
{
this.classDict = initPossibleClasses(this.propathCls, this.propath, this.caseSens);
WorkArea wa = locate();
if (!wa.legacyClassesLoaded)
{
wa.legacyClassesLoaded = true;
loadLegacyClasses(this);
}
}
}
/**
* Constructor to copy state for runtime conversion, from an exemplar instance.
*
* @param sym
* The exemplar instance.
*/
private SymbolResolver(SymbolResolver sym)
{
// initialize only the dictionaries which are used for runtime conversion
kwDict = sym.kwDict; // for runtime - this instance is immutable here
accumDict = null;
varDict = new ScopedSymbolDictionary<>(null, false); // for runtime
varDict.copyAll(sym.varDict, false);
funcDict = new ScopedSymbolDictionary<>(null, false); // for runtime
funcDict.copyAll(sym.funcDict, false);
varDefCache = new HashMap<>(); // TODO: is it used?
procDict = null;
explPkgDict = Collections.emptyMap();
searchPkgDict = Collections.emptySet();
labelDict = null;
widgetDict = null;
frameDict = null;
fieldWidgetDict = null;
useDictExps = null;
menuDict = null;
menuItemDict = null;
streamDict = null;
qryDict = new ScopedSymbolDictionary<>(null, false); // used by query conversion
dsetDict = null;
dsrcDict = null;
drelDict = null;
attributes = sym.attributes; // for runtime - this instance is immutable here
attrTypes = sym.attrTypes; // for runtime - this instance is immutable here
currentCls = null;
this.propathCls = Collections.emptyMap();
this.classDict = null;
this.caseSens = true;
this.silent = true;
setPropath(null);
}
/**
* Resolve the {@link ClassDefinition} for the specified AST, referencing it. It is assumed that the
* AST either has a <code>qualified_oo_name</code> annotation or otherwise, the AST is for a .cls file.
*
* @param ast
* The ast for which we need the {@link ClassDefinition}.
*
* @return See above.
*/
public static ClassDefinition resolveClassDefinition(Aast ast)
{
if (Configuration.isRuntimeConfig())
{
// runtime conversion does not allow .cls usage
return null;
}
ClassDefinition clsDef = null;
String qname = (String) ast.getAnnotation("qualified_oo_name");
if (qname == null)
{
// 'qualified_oo_name' may not have been set yet
String fname = ast.getFilename();
if (fname.toLowerCase().endsWith(".cls"))
{
try
{
clsDef = SymbolResolver.loadConvertedClass(fname, false);
}
catch (ClassNotFoundException e)
{
// ignore
}
}
}
else
{
// use the fast way to resolve it
clsDef = SymbolResolver.loadClassDefinition(qname);
}
return clsDef;
}
/**
* The parse for this entire file set has finished, and a final cleanup of used resources can be done for
* each {@link ClassDefinition}.
*/
public static void parseFinished()
{
WorkArea wa = locate();
if (wa.classCache != null)
{
wa.classCache.values().forEach(cls -> cls.parseFinished(true));
}
for (ScopedSymbolDictionary dict : wa.propathClassDicts.values())
{
for (int i = 0; i < dict.size(); i++)
{
Map classes = (Map) dict.getDictionaryAtScope(i, false);
if (classes != null)
{
classes.values().forEach(cls -> ((ClassDefinition) cls).parseFinished(true));
}
}
}
}
/**
* Sets 4gl sources defined in file-cvt-list.txt.
*
* @param fileList
* Set with the names of the sources.
*/
public static void setCvtSources (Set<String> sources)
{
WorkArea wa = locate();
if (wa.cvtSources != null)
{
return;
}
wa.cvtSources = sources;
}
/**
* Create a new instance which has only dictionaries used by the runtime conversion, initialized from the
* {@link #SYMBOL_RESOLVER_EXEMPLAR}.
*
* @return See above.
*/
public static SymbolResolver newRuntimeInstance()
{
return new SymbolResolver(SYMBOL_RESOLVER_EXEMPLAR);
}
/**
* Override the default skeleton path if it is configured.
*/
public static synchronized void initSkeletonPath()
{
skeletonPath = Configuration.getParameter("oo-skeleton-path");
// only rework these from the defaults if a skeleton path is overridden
if (skeletonPath != null)
{
if (!skeletonPath.endsWith(File.separator))
{
skeletonPath = skeletonPath + File.separator;
}
oo4glPath = skeletonPath + OO4GL_PATH;
dotnetPath = skeletonPath + DOTNET_PATH;
}
}
/**
* Obtain the list of all "fakeout" class and interface definitions that
* exist in the project. These <code>.cls</code> files represent the
* Progress built-ins and all .NET classes/interfaces that can be accessed.
* <p>
* These files will be listed in the directories represented by the
* {@link #OO4GL_PATH}, {@link #DOTNET_PATH} and the {@link #ASSEMBLY_PATH}.
*
* @return The list of <code>.cls</code> files, in the same case in which
* they exist in the filesystem.
*/
public static String[] listFakeoutFiles()
{
String[] oo4gl = getClassFileList(oo4glPath, false);
String[] dotnet = getClassFileList(dotnetPath, false);
String[] assembly = getClassFileList(ASSEMBLY_PATH, false);
ArrayList<String> list = new ArrayList<>();
list.addAll(Arrays.asList(oo4gl));
list.addAll(Arrays.asList(dotnet));
list.addAll(Arrays.asList(assembly));
return list.toArray(new String[0]);
}
/**
* Process a method definition to extract the signature.
*
* @param def
* The METHOD_DEF node which is used to extract the signature of the method.
*
* @return An array of the parameter types in signature order from left to right.
*/
public static ParameterKey[] calculateDefinitionSignature(Aast def)
{
List<ParameterKey> parms = new LinkedList<ParameterKey>();
processDefinitionSignature(def, (Aast ast, ParameterKey key, int idx) -> parms.add(key));
return parms.toArray(new ParameterKey[0]);
}
/**
* Process a method definition signature (left to right) executing the given consumer for each parameter.
*
* @param def
* The METHOD_DEF node whose signature is processed.
* @param handler
* The consumer of each parameter.
*/
public static void processDefinitionSignature(Aast def, ParameterConsumer handler)
{
if (def != null)
{
Aast methkw = null;
Aast lparens = null;
int defType = def.getType();
if (defType == METHOD_DEF)
{
methkw = def.getImmediateChild(KW_METHOD, null);
}
else if (defType == CONSTRUCTOR)
{
methkw = def.getImmediateChild(KW_CONSTRUC, null);
}
else
{
methkw = def;
if (defType != KW_METHOD && defType != KW_CONSTRUC && defType != EVENT_SIGNATURE)
{
System.out.printf("BAD METHOD NODE %s\n", def.dumpTree());
return;
}
}
int k = 0;
lparens = methkw.getImmediateChild(LPARENS, null);
if (lparens != null && lparens.getFirstChild() != null)
{
// iterate all parameters and extract their types
Aast parm = (Aast) lparens.getFirstChild();
while (parm != null)
{
if (parm.getType() == PARAMETER)
{
String name = translateType((int) ((long) parm.getAnnotation("type")), parm, null);
name = formatExtentName(name, readExtent(parm));
if (name == null)
{
System.out.printf("BAD PARM NODE %s\n", parm.dumpTree());
}
Long ptype = (Long) parm.getAnnotation("parmtype");
if (name != null && name.startsWith("BUFFER "))
{
ptype = null;
}
// at definition time, the default mode (if missing) is INPUT
handler.accept(parm,
new ParameterKey(ptype == null ? KW_INPUT : ptype.intValue(), name),
k);
}
parm = (Aast) parm.getNextSibling();
}
}
}
}
/**
* Read the extent value for the given node if it is specified as an annotation or if there
* is a KW_EXTENT child.
*
* @param node
* The AST to read.
*
* @return The extent value or 0 if this is a scalar node.
*/
public static int readExtent(Aast node)
{
if (Boolean.TRUE.equals(node.getAnnotation("dotnet-cls")) &&
Boolean.TRUE.equals(node.getAnnotation("dotnet-array")))
{
return -1;
}
int extent = 0;
if (node.getType() == PARAMETER && !node.downPath(KW_AS))
{
node = (Aast) node.getFirstChild();
int type = node.getType();
while (type == KW_INPUT ||
type == KW_IN_OUT ||
type == KW_OUTPUT ||
type == KW_APPEND ||
type == KW_BIND ||
type == KW_BY_REF ||
type == KW_BY_VALUE)
{
node = (Aast) node.getNextSibling();
type = node.getType();
}
}
if (node.getType() == OBJECT_INVOCATION)
{
// the rightmost child of the topmost OBJECT_INVOCATION is the
// rightmost node in the chain
node = node.getChildAt(1);
}
// after finding the correct node above, check if this is a subscript ref
if (node.downPath(LBRACKET))
{
// this is an indexed reference
return 0;
}
if (node.isAnnotation("extent"))
{
extent = (int) ((long) node.getAnnotation("extent"));
}
else if (node.getType() == EXPRESSION && node.getNumChildren() > 0)
{
Aast child = node.getChildAt(0);
if (child != null && child.isAnnotation("extent"))
{
extent = (int) ((long) child.getAnnotation("extent"));
}
}
else
{
Aast child = node.getImmediateChild(KW_EXTENT, null);
if (child != null)
{
// there is an extent, default to indeterminate
extent = -1;
Aast num = child.getChildAt(0);
if (num != null)
{
extent = ExpressionConversionWorker.literalToInt(num);
}
}
}
return extent;
}
/**
* Process the extent value and augment the name specification with the extent value
* if it is not scalar. Indeterminate extent (-1) will have an "[]" appended. All other
* non-scalar values (not 0) will be appended with the fixed extent "[num]" where num
* is the value of the extent.
*
* @param name
* The data type name to augment.
* @param extent
* The extent value.
*
* @return The augmented name if this is a non-scalar value and the passed in name if
* scalar.
*/
public static String formatExtentName(String name, int extent)
{
if (extent != 0 && extent != -2)
{
if (extent == -1)
{
// indeterminate
name = String.format("%s[]", name);
}
else
{
// fixed
name = String.format("%s[%d]", name, extent);
}
}
return name;
}
/**
* Compute the Java signature for a legacy method - this will include the converted method name and the
* parameter representation in the converted Java code, but not the return type.
*
* @param name
* The legacy name.
* @param sig
* The parameter definition.
*
* @return The signature using Java representation for the parameters.
*/
public static String calculateJavaSignature(String name, ParameterKey[] sig)
{
WorkArea wa = local.get();
String jname = wa.nconvert.convert(name, MatchPhraseConstants.TYPE_METHOD);
// in 4GL, the method names are case-insensitive - even if we want a Java signature here,
// we still need to use a lowercased name, as otherwise overridden methods may not be resolved
// properly (as 4GL has no restriction for an overridden method to have the same case as
// the parent method).
jname = jname.toLowerCase();
return jname.toLowerCase() + "(" + calculateJavaParameters(sig) + ")";
}
/**
* Compute the Java signature for the legacy method parameters (does not include the parenthesis).
*
* @param sig
* The parameter definition.
*
* @return The Java representation of the parameter list or "" if there are no parameters.
*/
public static String calculateJavaParameters(ParameterKey[] sig)
{
String core = "";
if (sig != null)
{
for (int i = 0; i < sig.length; i++)
{
core = core + (i == 0 ? "" : ", ") + sig[i].toJava();
}
}
return core;
}
/**
* Given a Java method with a {@link LegacySignature}, reverse-compute its 4GL signature.
*
* @param mname
* The legacy method name.
* @param lsig
* The legacy signature annotation.
*/
public static String calculateLegacySignature(String mname, LegacySignature lsig)
{
LegacyParameter[] lparms = lsig.parameters();
ParameterKey[] sig = new ParameterKey[(lparms == null) ? 0 : lparms.length];
for (int i = 0; i < lparms.length; i++)
{
sig[i] = new ParameterKey(lparms[i]);
}
return calculateLegacySignature(mname, sig);
}
/**
* Compute the legacy signature for a legacy method - this will include the legacy name (lowercased) and
* the parameter list, but not the return type.
*
* @param name
* The legacy name.
* @param sig
* The parameter definition.
*
* @return The legacy signature representation.
*/
public static String calculateLegacySignature(String name, ParameterKey[] sig)
{
String s = name.toLowerCase() + "(";
if (sig != null)
{
for (ParameterKey pk : sig)
{
s = s + (s.endsWith("(") ? "" : ", ") + pk.toString();
}
}
return s + ")";
}
/**
* Given a class property or a class event, track all its associated FWD methods, to allow
* disambiguation during parsing.
* <p>
* This is used for incremental conversion, where we need to know the details of this method,
* when they are defined in an already converted class.
*
* @param stype
* Token type of the defining statement.
* @param cls
* The class in which the property or event exists.
* @param node
* The AST node being processed.
* @param var
* The associated variable for this member.
* @param name
* The legacy name.
*/
public static void trackPropertyOrEventMethodSignatures(int stype,
ClassDefinition cls,
Aast node,
Variable var,
String name)
{
if (isPreScan() && !cls.isBuiltIn())
{
return;
}
boolean over = var.isOverride();
boolean abst = var.isAbstract();
if (stype == DEFINE_PROPERTY)
{
String ptype = translateType(var.getTokenType(), null, var.getClassName());
if (var.isExtent())
{
String ext = "[" + (var.getExtent() > 0 ? Integer.toString(var.getExtent()) : "") +"]";
cls.registerPropertyMethod(node, "bulk-get", name, over, abst, new ParameterKey[0]);
cls.registerPropertyMethod(node,
"get",
name,
over,
abst,
new ParameterKey[]
{
new ParameterKey(KW_INPUT, "int64")
});
cls.registerPropertyMethod(node,
"bulk-set",
name,
over,
abst,
new ParameterKey[]
{
new ParameterKey(KW_INPUT, ptype + ext)
});
cls.registerPropertyMethod(node,
"set",
name,
over,
abst,
new ParameterKey[]
{
new ParameterKey(KW_INPUT, ptype),
new ParameterKey(KW_INPUT, "int64")
});
cls.registerPropertyMethod(node,
"set-all",
name,
over,
abst,
new ParameterKey[]
{
new ParameterKey(KW_INPUT, ptype)
});
cls.registerPropertyMethod(node, "length-of", name, over, abst, new ParameterKey[0]);
cls.registerPropertyMethod(node,
"resize",
name,
over,
abst,
new ParameterKey[]
{
new ParameterKey(KW_INPUT, "int64")
});
}
else
{
cls.registerPropertyMethod(node, "get", name, over, abst, new ParameterKey[0]);
cls.registerPropertyMethod(node,
"set",
name,
over,
abst,
new ParameterKey[]
{
new ParameterKey(KW_INPUT, ptype)
});
}
}
else if (stype == DEFINE_EVENT)
{
cls.registerPropertyMethod(node, "subscribe", name, over, abst,
new ParameterKey[]
{
new ParameterKey(KW_INPUT, "handle"),
new ParameterKey(KW_INPUT, "character"),
});
cls.registerPropertyMethod(node, "unsubscribe", name, over, abst,
new ParameterKey[]
{
new ParameterKey(KW_INPUT, "handle"),
new ParameterKey(KW_INPUT, "character"),
});
cls.registerPropertyMethod(node, "subscribe", name, over, abst,
new ParameterKey[]
{
new ParameterKey(KW_INPUT, "object<? extends progress.lang.object>"),
new ParameterKey(KW_INPUT, "character"),
});
cls.registerPropertyMethod(node, "unsubscribe", name, over, abst,
new ParameterKey[]
{
new ParameterKey(KW_INPUT, "object<? extends progress.lang.object>"),
new ParameterKey(KW_INPUT, "character"),
});
// TODO: publish-[evtName] isn't tracked - not sure if even is required to do so
// cls.registerPropertyMethod(node, "publish-", name, over, abst, pkey);
}
}
/**
* Convert the given legacy method name to Java and return it.
*
* @param name
* The the 4GL method name.
*
* @return The converted Java method name.
*/
public static String convertMethodName(String name)
{
WorkArea wa = locate();
return wa.nconvert.convert(name, MatchPhraseConstants.TYPE_METHOD);
}
/**
* Translate a variable type into a wrapper type name for use in a signature specification.
*
* @param type
* The token type representing a variable/parameter type.
* @param parm
* The PARAMETER node which is used to extract the type of this parameter.
* @param qualified
* Fully qualified object classname or {@code null} if no qualifier is needed.
*
* @return The wrapper class name.
*/
public static String translateType(int type, Aast parm, String qualified)
{
if (parm != null)
{
if (parm.downPath(KW_DSET_HND) || parm.getType() == KW_DSET_HND)
{
return "DATASET-HANDLE";
}
else if (parm.downPath(KW_TAB_HAND) || parm.getType() == KW_TAB_HAND)
{
return "TABLE-HANDLE";
}
}
String cls = null;
Aast ref = null;
switch (type)
{
case VAR_CHAR:
cls = "character";
break;
case VAR_COM_HANDLE:
cls = "comhandle";
break;
case VAR_DATE:
cls = "date";
break;
case VAR_DATETIME:
cls = "datetime";
break;
case VAR_DATETIME_TZ:
cls = "datetimetz";
break;
case VAR_DEC:
cls = "decimal";
break;
case VAR_HANDLE:
cls = "handle";
break;
case VAR_INT:
cls = "integer";
break;
case VAR_INT64:
cls = "int64";
break;
case VAR_LOGICAL:
cls = "logical";
break;
case VAR_LONGCHAR:
cls = "longchar";
break;
case VAR_MEMPTR:
cls = "memptr";
break;
case VAR_RAW:
cls = "raw";
break;
case VAR_RECID:
cls = "recid";
break;
case VAR_ROWID:
cls = "rowid";
break;
case VAR_POLY:
cls = "Object";
break;
case VAR_CLASS:
cls = "object";
String qual = qualified;
if (parm != null)
{
if (parm.isAnnotation("is-java") && (Boolean) parm.getAnnotation("is-java"))
{
cls = "jobject";
}
if (qualified == null)
{
qual = (String) parm.getAnnotation("qualified");
}
}
cls = String.format("%s<? extends %s>", cls, qual);
break;
// TABLE and BUFFER parameters - these will have the TEMP-TABLE or BUFFER prefix,
// followed by the schemaname or other details that make the table unambigous
case KW_TABLE:
ref = parm.getType() == KW_TABLE ? parm : parm.getImmediateChild(KW_TABLE, null);
if (ref.downPath(TEMP_TABLE))
{
ref = ref.getImmediateChild(TEMP_TABLE, null);
}
else if (ref.downPath(WORK_TABLE))
{
ref = ref.getImmediateChild(WORK_TABLE, null);
}
else if (ref.downPath(BUFFER))
{
ref = ref.getImmediateChild(BUFFER, null);
}
else
{
// this is unexpected since permanent tables and work-tables should not be possible here
// and buffers are handled below
throw new RuntimeException(String.format("TABLE parameters should be temp-tables: %s",
parm.dumpTree()));
}
String signature = (String) ref.getAnnotation("db_signature");
if (signature == null)
{
throw new RuntimeException(String.format("Missing 'db_signature' annotation in: %s",
ref.dumpTree()));
}
// for temp-tables, the compile time mapping is done via the table's field signature
cls = String.format("TEMP-TABLE <%s>", signature);
break;
case KW_BUFFER:
ref = parm.getType() == KW_BUFFER ? parm : parm.getImmediateChild(KW_BUFFER, null);
if (ref.downPath(KW_FOR))
{
ref = ref.getImmediateChild(KW_FOR, null);
}
ref = (Aast) ref.getFirstChild();
if (ref.getType() == TEMP_TABLE ||
(ref.getType() == BUFFER && "".equals(ref.getAnnotation("dbname"))))
{
String tabname = (String) ref.getAnnotation("schemaname");
String tabsig = (String) ref.getAnnotation("db_signature");
if (tabsig == null)
{
throw new RuntimeException(String.format("Missing 'db_signature' annotation in: %s",
ref.dumpTree()));
}
cls = String.format("BUFFER <temp-table %s <%s>>", tabname.toLowerCase(), tabsig);
}
else
{
cls = String.format("BUFFER <%s>", ref.getAnnotation("schemaname"));
}
break;
case KW_DATASET:
ref = parm.getType() == KW_DATASET ? parm : parm.getImmediateChild(KW_DATASET, null);
if (ref == null)
{
throw new RuntimeException(String.format("Missing KW_DATASET in: %s", parm.dumpTree()));
}
if (!ref.downPath(DATA_SET))
{
throw new RuntimeException(String.format("Missing DATA_SET in: %s", parm.dumpTree()));
}
ref = (Aast) ref.getImmediateChild(null, DATA_SET);
String sig = (String) ref.getAnnotation("db_signature");
if (sig == null)
{
throw new RuntimeException(String.format("Missing 'db_signature' annotation in: %s",
ref.dumpTree()));
}
cls = String.format("DATASET %s", sig);
break;
case KW_DATA_SRC:
// TODO: as far as I know, this is not possible; dump some data so we can generate an example
// with this as parameter and argument
LOG.log(Level.WARNING, "UNEXPECTED DATA-SOURCE PARAMETER " + parm.dumpTree());
cls = "DATA-SOURCE";
break;
}
return cls;
}
/**
* Process a method call to extract the signature.
*
* @param call
* The OO_METH_* node which is used to extract the signature of the method.
*
* @return An array of the parameter types in signature order from left to right.
*/
public static ParameterKey[] calculateCallSignature(Aast call)
{
ParameterKey[] signature = new ParameterKey[0];
if (call != null)
{
ArrayList<ParameterKey> list = new ArrayList<>();
// iterate all parameters and extract their types
Aast child = (Aast) call.getFirstChild();
if ((call.isAnnotation("oldtype") && (long) call.getAnnotation("oldtype") == KW_NEW) ||
call.getType() == KW_THIS_OBJ ||
call.getType() == KW_SUPER)
{
child = (Aast) call.getImmediateChild(LPARENS, null);
child = (Aast) child.getFirstChild();
}
Integer parmtype = null;
int parIdx = 0;
while (child != null)
{
int type = child.getType();
if (type == CLASS_EVENT || type == KW_PUBLISH)
{
child = (Aast) child.getNextSibling();
continue;
}
Aast parm = child;
if (type == KW_INPUT || type == KW_IN_OUT || type == KW_OUTPUT)
{
parmtype = type;
}
else if (type == PARAMETER)
{
parm = (Aast) child.getFirstChild();
while (parm != null)
{
type = parm.getType();
if (type != KW_INPUT &&
type != KW_IN_OUT &&
type != KW_OUTPUT &&
type != KW_APPEND &&
type != KW_BIND &&
type != KW_BY_REF &&
type != KW_BY_VALUE)
{
break;
}
if (type == KW_INPUT || type == KW_IN_OUT || type == KW_OUTPUT)
{
parmtype = type;
}
parm = (Aast) parm.getNextSibling();
}
}
// avoid the optional mode or table parm modifiers
if (type != KW_INPUT &&
type != KW_IN_OUT &&
type != KW_OUTPUT &&
type != KW_APPEND &&
type != KW_BIND &&
type != KW_BY_REF &&
type != KW_BY_VALUE)
{
String cls = ExpressionConversionWorker.expressionType(parm, false, false);
// this is not just buffers, it is also any form of table reference except for table-handle
if ("Buffer".equalsIgnoreCase(cls) || "DATASET".equalsIgnoreCase(cls))
{
cls = translateType(parm.getType(), parm, null);
}
else
{
// non-buffers get checked for extent
cls = formatExtentName(cls, readExtent(parm));
}
Aast pref = parm.getType() == OBJECT_INVOCATION ? parm.getChildAt(1) : parm;
// constructors always return the expected type, so do not wrap the parameter
boolean isCtor = pref.getType() == ProgressParserTokenTypes.FUNC_CLASS &&
((Long) pref.getAnnotation("oldtype")) == ProgressParserTokenTypes.KW_NEW;
if (!isCtor && pref.isAnnotation("dynamic") && (Boolean) pref.getAnnotation("dynamic"))
{
pref.putAnnotation("wrap_parameter", true);
pref.putAnnotation("classname", cls);
}
if (parmtype == null &&
parm.isAnnotation("is-literal") &&
(Boolean) parm.getAnnotation("is-literal"))
{
parmtype = KW_INPUT;
}
boolean fromExpression = ExpressionConversionWorker.hasOperators(parm);
// the parameter type is not defaulting to INPUT (as in func/proc call cases).
// OO method call resolution is done by checking the types first, and if more than
// one match found, the modes are checked second.
Aast mcall = parm;
if (mcall.getType() == OBJECT_INVOCATION)
{
mcall = parm.getChildAt(1);
}
boolean dynamicPolyArg = mcall.isAnnotation("dynamic-poly");
list.add(new ParameterKey(parmtype, cls, fromExpression, dynamicPolyArg));
// reset the parameter type here
parmtype = null;
parm.putAnnotation("param_index", (long) parIdx);
if (parm.getType() == PARAMETER)
{
Aast ref = parm.getChildAt(parm.getNumImmediateChildren() - 1);
ref.putAnnotation("param_index", (long) parIdx);
}
parIdx = parIdx + 1;
}
child = (Aast) child.getNextSibling();
}
signature = list.toArray(signature);
}
return signature;
}
/**
* Save all the 4GL classes (builtin or business logic), .NET or Java classes encountered
* during parsing.
*/
public static void saveClasses()
{
WorkArea wa = local.get();
for (String key : wa.propathClassDicts.keySet())
{
ScopedSymbolDictionary dict = wa.propathClassDicts.get(key);
Iterator<String> iter = dict.symbolSet().iterator();
while (iter.hasNext())
{
String clsName = iter.next();
ClassDefinition clsDef = (ClassDefinition) dict.getSymbolAtScope(clsName, -1);
ConversionData.saveClass(clsDef);
}
}
}
/**
* Load all legacy (skeleton) classes which were previously parsed.
*
* @param sym
* The symbol resolver.
*/
public static void loadLegacyClasses(SymbolResolver sym)
{
if (Configuration.isRuntimeConfig())
{
// no-op in runtime conversion mode
return;
}
List<String> classes = ConversionData.listLegacyClasses();
for (String qname : classes)
{
loadClassDefinition(sym, qname);
}
}
/**
* Load the specified class definition.
*
* @param qname
* The legacy OO class qualified name.
*
* @return The found converted class.
*
* @throws ClassNotFoundException
* If the class can't be resolved or loaded.
*/
public static ClassDefinition loadConvertedClassByQname(String qname)
throws ClassNotFoundException
{
String fname = ConversionData.findClassFilename(qname);
if (fname == null)
{
throw new ClassNotFoundException("Can not find source filename for qualfied class " + qname);
}
return loadConvertedClass(fname, false);
}
/**
* Load the specified class definition. This will either create a {@link ClassDefinition}
* structure for a new class (to be later populated with members by the parser), or will restore
* an instance from an already-converted class.
*
* @param filename
* The file name for the legacy OO class.
* @param isNew
* Flag indicating that this is a new class (now being parsed), or a previously
* converted class.
*
* @return The class definition.
*
* @throws ClassNotFoundException
* If the class can't be resolved or loaded.
*/
public static ClassDefinition loadConvertedClass(String filename, boolean isNew)
throws ClassNotFoundException
{
WorkArea wa = local.get();
String qname = wa.fname2qname.get(filename);
if (qname == null)
{
qname = ConversionData.findClassQName(filename);
if (qname == null)
{
throw new ClassNotFoundException("Can not find class associated with " + filename);
}
wa.fname2qname.put(filename, qname);
}
ClassDefinition clsdef = null;
if (isNew)
{
clsdef = getClassDefinition(qname);
if (clsdef == null)
{
throw new ClassNotFoundException("Can not find class " + qname + " from " + filename);
}
}
else
{
clsdef = loadClassDefinition(qname);
}
return clsdef;
}
/**
* Load the specified class, given by its qualified name. This will return either a
* {@link ClassDefinition} instance being parsed in the current run, or load the definition
* from an already parsed and converted .ast file. If this file is not found, it will return
* <code>null</code>
*
* @param qname
* The qualified class name.
*
* @return See above.
*/
public static ClassDefinition loadClassDefinition(String qname)
{
return loadClassDefinition(null, qname);
}
/**
* Load the specified class, given by its qualified name. This will return either a
* {@link ClassDefinition} instance being parsed in the current run, or load the definition
* from an already parsed and converted .ast file. If this file is not found, it will return
* <code>null</code>
*
* @param sym
* The symbol resolver. Non-null only during parsing.
* @param qname
* The qualified class name.
*
* @return See above.
*/
public static ClassDefinition loadClassDefinition(SymbolResolver sym, String qname)
{
return loadClassDefinition(sym, qname, true);
}
/**
* Load the specified class, given by its qualified name. This will return either a
* {@link ClassDefinition} instance being parsed in the current run, or load the definition
* from an already parsed and converted .ast file. If this file is not found, it will return
* <code>null</code>
*
* @param sym
* The symbol resolver. Non-null only during parsing.
* @param qname
* The qualified class name.
* @param failOnPending
* When this flag is set, if the class is pending to be loaded, then throw an exception.
*
* @return See above.
*/
public static ClassDefinition loadClassDefinition(SymbolResolver sym, String qname, boolean failOnPending)
{
WorkArea wa = local.get();
// this checks for classes converted in the current run
ClassDefinition clsDef = getClassDefinition(qname);
if (clsDef != null)
{
return clsDef;
}
String file = ConversionData.findClassFilename(qname);
if (file == null || !new File(file + ".ast").exists())
{
return null;
}
String key = qname.toLowerCase();
if (wa.pendingLoading.contains(key))
{
if (failOnPending)
{
throw new RuntimeException("Class " + qname + " is pending loading.");
}
return null;
}
try
{
wa.pendingLoading.add(key);
clsDef = new ClassDefinition(sym, qname);
}
catch (SchemaException e)
{
throw new RuntimeException(e);
}
finally
{
wa.pendingLoading.remove(key);
}
wa.classCache.put(qname.toLowerCase(), clsDef);
return clsDef;
}
/**
* Given a qualified legacy class name, resolve its definition.
*
* @param clsName
* The qualified class name.
*
* @return The found definition, or <code>null</code> if it is not yet loaded.
*/
public static ClassDefinition getClassDefinition(String clsName)
{
WorkArea wa = local.get();
ClassDefinition clsDef = wa.classCache.get(clsName.toLowerCase());
if (clsDef != null)
{
return clsDef;
}
for (String key : wa.propathClassDicts.keySet())
{
ScopedSymbolDictionary dict = wa.propathClassDicts.get(key);
clsDef = (ClassDefinition) dict.getSymbolAtScope(clsName, -1);
if (clsDef != null)
{
return clsDef;
}
}
return null;
}
/**
* List the 4GL class/interface files in a given path.
*
* @param path
* The directory to search.
* @param caseSens
* <code>true</code> if the legacy system had a case-sensitive
* file-system.
*
* @return The list of files that exist. If there are no files, the
* array will have a 0 length.
*/
public static String[] getClassFileList(String path, boolean caseSens)
{
File dir = new File(path);
if (!dir.exists() || !dir.isDirectory())
{
return new String[0];
}
String absPath = null;
try
{
absPath = dir.getCanonicalPath();
}
catch (IOException exc)
{
LOG.log(Level.SEVERE, "", exc);
}
String[] files = DIR_CLASSES.get(absPath);
if (files == null)
{
FileList fl = new FileSpecList(dir, CLASS_SPLAT, true, caseSens);
files = fl.listFilenames();
for (int i = 0; i < files.length; i++)
{
String l = files[i];
l = Configuration.normalizeFilename(l);
files[i] = l;
}
DIR_CLASSES.put(absPath, files);
}
return files;
}
/**
* Store each element of the list as a key in the map and create a filename
* as the mapped value (using the path + the key). The keys will be
* lowercased if case-insensitive matching is configured.
*
* @param map
* The map to modify.
* @param list
* The elements to add.
* @param path
* The root path in which all elements reside.
* @param caseSens
* <code>true</code> if the legacy system had a case-sensitive
* file-system.
*/
public static void createClassMappings(Map<String, List<String>> map,
String[] list,
String path,
boolean caseSens)
{
path = Configuration.normalizeFilename(path);
int begin = path.length();
if (path.equals("."))
{
// remove the leading "/" or "\" char
begin = 2;
}
else if (!path.endsWith("\\") && !path.endsWith("/"))
{
begin++;
}
for (int i = 0; i < list.length; i++)
{
String l = list[i];
int end = l.length() - 4;
String entry = l.substring(begin, end);
// only the first entry is kept, in the propath - the others will not be seen
String key = caseSens ? entry : entry.toLowerCase();
List<String> propathList = map.get(key);
if (propathList == null)
{
propathList = new ArrayList<>();
map.put(key, propathList);
}
propathList.add(l);
}
}
/**
* Check if we are in pre-scan mode.
*
* @return <code>true</code> if we are in pre-scan mode.
*/
public static boolean isPreScan()
{
return locate().preScanPass > 0;
}
/**
* Find the converted Java name for a legacy builtin class.
* <p>
* On first run, this will go through all FWD implementations of {@link _BaseObject_} in the
* <code>com.goldencode.p2j.oo</code> package, and map the legacy class name to its definition
* name.
*
* @param name
* The legacy class name.
*
* @return The converted Java name which must be used, or <code>null</code> if FWD does not
* provide yet a definition for it.
*/
static String findConvertedLegacyClass(String name)
{
WorkArea wa = locate();
if (wa.legacyToJava == null)
{
wa.legacyToJava = new HashMap<>();
Reflections reflections = new Reflections("com.goldencode.p2j.oo");
Set<Class<? extends _BaseObject_>> legacyClasses =
reflections.getSubTypesOf(_BaseObject_.class);
for (Class<? extends _BaseObject_> cls : legacyClasses)
{
LegacyResource lr = cls.getAnnotation(LegacyResource.class);
if (lr == null)
{
System.out.println("WARNING: class " + cls +
" doesn't have a LegacyResource annotation!");
}
else
{
String clsName = cls.getName();
if (cls == BaseObject.class)
{
clsName = _BaseObject_.class.getName();
}
wa.legacyToJava.put(lr.resource().toLowerCase(), clsName);
// check the method names against NameConverter
for (Method m : cls.getDeclaredMethods())
{
if (m.isAnnotationPresent(LegacySignature.class) &&
NameConverter.isInternalMethodName(m.getName()))
{
String msg = "Skeleton Java method name collides with FWD internal API: " +
cls.getName() + "." + m.toString();
LOG.log(Level.SEVERE, msg);
System.exit(1);
}
}
}
}
}
return wa.legacyToJava.get(name.toLowerCase());
}
/**
* Get the context-local {@link WorkArea} instance.
*
* @return See above.
*/
private static WorkArea locate()
{
return local.get();
}
/**
* Get a brief descriptor of the database type with the specified logical
* name or alias. This is for CONVERSION TIME ONLY. If schema support is
* not loaded then we assume the database is OK.
*
* @param name
* Logical name or alias of the target database.
*
* @return The string "PROGRESS" or unknown value if <code>name</code>
* is not a recognized logical name or alias. If the schema
* dictionary is not loaded, then this will simply return
* "PROGRESS".
*/
public character dbtype(character name)
{
if (schemaDict != null && name != null)
{
boolean valid = (!name.isUnknown() && schemaDict.isDatabase(name.toStringMessage()));
if (!valid)
{
return (new character());
}
}
return new character("PROGRESS");
}
/**
* Check if this numeric literal can fit 32 bits or 64 bits.
*
* @param ast
* The NUM_LITERAL node.
*/
public void annotateNumericLiteral(Aast ast)
{
String text = ast.getText();
if (text.endsWith("+") || text.endsWith("-"))
{
ast.putAnnotation("original-text", text);
if (text.endsWith("-"))
{
text = "-" + text;
}
// remove last
text = text.substring(0, text.length() - 1);
ast.setText(text);
}
// following approach is duplicated from cleanup.rules NUM_LITERAL rule
Long val = null;
if (ast.getType() == NUM_LITERAL)
{
val = ExpressionConversionWorker.parseLongQuiet(text);
}
else if (ast.getType() == HEX_LITERAL)
{
if (ExpressionConversionWorker.isHexLiteralValid(text))
{
decimal dec = decimal.fromUnsignedHexLiteral(text);
if (CompareOps._isEqual(dec, dec.longValue()))
{
val = dec.longValue();
}
else
{
// this is actually a DEC_LITERAL
}
}
}
if (val != null)
{
if (val < -2147483648l || val > 2147483647l)
{
ast.putAnnotation("use64bit", true);
}
}
}
/**
* Set the 'in-method body' state.
*
* @param inMethod
* The new state for the {@link #inMethod} flag.
*/
public void setInMethod(boolean inMethod)
{
this.inMethod = inMethod;
}
/**
* 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)
{
return schemaDict.setUniqueFieldMatch(unique);
}
/**
* Set the {@link #indexFieldSearch} flag, marking the beginning (and possibility) for BY fields
* to be matched against an unique index.
*/
public void startIndexFieldSearch()
{
indexFieldSearch = true;
}
/**
* Terminate the index field search; this means that an {@link #foundUniqueIndex unique index}
* was matched or that we encountered a condition which doesn't allow unique index matching
* (i.e. currently collected {@link #sortFields fields} are not a prefix for any
* {@link #indexes} in {@link #indexTable}, BREAK clause was used, multiple tables in the
* query, etc.
*/
public void endIndexFieldSearch()
{
if (!indexFieldSearch)
{
foundUniqueIndex = false;
return;
}
indexFieldSearch = false;
useIndexFieldSearch = false;
foundUniqueIndex = false;
indexTable = null;
indexBuffer = null;
sortFields = null;
indexes = null;
}
/**
* Save the {@link #indexTable table} used to match the BY sort order against unique indexes.
* If there are no unique indexes in this table, then {@link #endIndexFieldSearch()}.
* <p>
* All indexes collected in {@link #indexes} will be unique and no other index will match as a
* prefix for another index.
*
* @param recordAst
* The table's AST reference.
*/
public void setIndexFieldSearchTable(Aast recordAst)
{
if (!indexFieldSearch)
{
return;
}
Aast fieldRef = (Aast) recordAst.getFirstChild();
this.indexTable = (String) fieldRef.getAnnotation("schemaname");
this.indexBuffer = (String) fieldRef.getAnnotation("bufname");
// load the unique indexes in this table
Aast table = null;
try
{
table = schemaDict.getTable(indexTable);
}
catch (SchemaException e)
{
// TODO: OO requires lookup of temp-table up the hierarchy
endIndexFieldSearch();
return;
}
indexes = schemaDict.getUniqueIndexes(table);
if (indexes.isEmpty())
{
// no unique indexes, nothing to match, so disable this
endIndexFieldSearch();
}
else
{
sortFields = new ArrayList<>();
}
}
/**
* Try to collect the expression associated with the BY clause, only if this is a field
* reference and if, together with {@link #sortFields}, match a prefix in {@link #indexes},
* <p>
* Otherwise, the {@link #endIndexFieldSearch() match is terminated}. IF an unique index
* is fully matched, then {@link #foundUniqueIndex} flag is set, to allow any subsequent BY
* clauses to match garbage.
*
* @param expr
* The expression associated with the BY clause.
*/
public void collectSortField(Aast expr)
{
if (!indexFieldSearch || !useIndexFieldSearch)
{
return;
}
int etype = expr.getType();
if (etype == EXPRESSION)
{
etype = expr.getFirstChild().getType();
}
if (!(etype >= BEGIN_FIELDTYPES && etype <= END_FIELDTYPES))
{
// complex expression, can't use, fallback to field search...
endIndexFieldSearch();
}
else
{
// colect this for further analysis
sortFields.add(expr);
try
{
if (indexPrefixMatch(true))
{
// found an unique index, end index matching.
// every other BY clause will be marked with 'drop=true'
endIndexFieldSearch();
// we can start matching garbage from now on
foundUniqueIndex = true;
}
else if (!indexPrefixMatch(false))
{
// currently collected BY fields are not a prefix for any index, terminate search
endIndexFieldSearch();
}
}
catch (SchemaException e)
{
LOG.log(Level.WARNING,"", e);
}
}
}
/**
* Enable or disable field matching against {@link #indexes}, by setting the
* {@link #useIndexFieldSearch} flag. This is active only for the duration of
* {@link ProgressParser#sort_order_clause() a BY clause}.
*
* @param use
* Flag state.
*/
public void useIndexFieldSearch(boolean use)
{
if (!indexFieldSearch)
{
return;
}
this.useIndexFieldSearch = use;
}
/**
* Check if an unique index was found with the current BY clauses, or not.
*
* @return The state of the {@link #foundUniqueIndex} flag.
*/
public boolean isFoundUniqueIndex()
{
return foundUniqueIndex;
}
/**
* Set the {@link #localLookup flag} to allow check for a data member or method which is
* looked up without a qualifier.
*
* @param state
* The new flag state.
*/
public void setLocalLookup(boolean state)
{
this.localLookup = state;
}
/**
* Set the {@link #classLookup} flag. A <code>true</code> value will limit variable lookup
* only to class members (as we are in a object chain).
*
* @param state
* The new flag state.
*/
public void setClassLookup(boolean state)
{
this.classLookup = state;
}
/**
* Get the {@link #classLookup} flag.
*
* @return See above.
*/
public boolean isClassLookup()
{
return classLookup;
}
/**
* Enables schema dictionary lookups when they are disabled at construction
* and provides external control or sourcing over the schema dictionary.
*
* @param schemaDict
* The schema dictionary to use.
*/
public void setSchemaDictionary(SchemaDictionary schemaDict)
{
this.schemaDict = schemaDict;
}
/**
* Gets the <code>PROPATH</code>.
*
* @return The current <code>PROPATH</code> to honor.
*/
public String[] getPropath()
{
return propath;
}
/**
* Sets the <code>PROPATH</code>.
*
* @param propath
* The current <code>PROPATH</code> to honor or <code>null</code>
* for the default of ".".
*/
public void setPropath(String[] propath)
{
this.propath = (propath == null) ? new String[] { "." } : propath;
}
/**
* Adds a <code>Keyword</code> to the keyword dictionary.
*
* @param word
* <code>Keyword</code> to add.
*/
public void addKeyword(Keyword word)
{
kwDict.addKeyword(word);
}
/**
* Removes a <code>Keyword</code> from the keyword dictionary.
*
* @param word
* <code>Keyword</code> to remove.
*/
public void removeKeyword(Keyword word)
{
kwDict.deleteKeyword(word);
}
/**
* Searches for a <code>Keyword</code> in the keyword dictionary, based
* on a specific key.
*
* @param key
* Text string to match.
* @return The <code>Keyword</code> that was found or null if no match
* was found.
*/
public Keyword lookupKeyword(String key)
{
return kwDict.lookupKeyword(key);
}
/**
* Increases the scoping level of the variable and widget dictionaries by
* 1 level. All symbols must be unique within a scoping level.
*/
public void addScope()
{
varDict.addScope(null);
widgetDict.addScope(null);
fieldWidgetDict.addScope(null);
useDictExps.addScope(null);
dsrcDict.addScope(null);
dsetDict.addScope(null);
}
/**
* Decreases the scoping level of the variable and widget dictionaries by
* 1 level. All symbols previously defined in the top-most scoping level
* are lost and will no longer be found during lookups.
*/
public void deleteScope()
{
varDict.deleteScope();
widgetDict.deleteScope();
fieldWidgetDict.deleteScope();
useDictExps.deleteScope();
dsetDict.deleteScope();
dsrcDict.deleteScope();
}
/**
* Persist schema dictionary information which is specific to the source
* file currently being parsed.
*
* @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 persistSchemaData(String srcFile, String schemaFile)
throws AstException
{
if (schemaDict != null)
{
schemaDict.persist(srcFile, schemaFile);
}
}
/**
* Adds a variable name and associated token type to the appropriate scope
* of the variable dictionary, based on the content of the passed in
* <code>Variable</code> wrapper object.
*
* @param global
* Add to global scope if <code>true</code>.
* @param var
* Variable to add.
*/
public void addVariable(boolean global, Variable var)
{
addWrappedWorker(varDict, global, var.getName(), var);
}
/**
* Adds a widget name and associated token type to the appropriate scope
* of the widget dictionary, based on the content of the passed in
* <code>Variable</code> wrapper object.
*
* @param global
* Add to global scope if <code>true</code>.
* @param wid
* Widget as represented in a <code>Variable</code> object.
*/
public void addWidget(boolean global, Variable wid)
{
addWrappedWorker(widgetDict, global, wid.getName(), wid);
}
/**
* Helper to load variable, widget and frame dictionaries based on a list
* of <code>Variable</code> objects. This processing will load all 3
* dictionaries from the same list, however it must be noted that adding
* a variable does NOT result in a matching widget of the same name in
* the widget dictionary (even though this is essentially what happens
* when the parser adds new variables to the variable dictionary). To
* achieve this same effect, one must add multiple <code>Variable</code>
* objects to the list as needed.
* <p>
* A field can be added to this lookup by using a field token type instead
* of a variable token type. This will allow an ambiguous field reference
* to be made unambiguous since variable lookups are processed
* <b>before</b> any schema dictionary lookups.
*
* @param varlist
* List of variables, widgets, frames or fields to add to the
* appropriate dictionary.
*/
public void loadVariableSymbols(Variable[] varlist)
{
for ( int j = 0; j < varlist.length; j++ )
{
int type = varlist[j].getTokenType();
if (
(type >= ProgressParser.BEGIN_VARTYPES && type <= ProgressParser.END_VARTYPES) ||
(type >= ProgressParser.BEGIN_FIELDTYPES && type <= ProgressParser.END_FIELDTYPES)
)
{
addVariable(true, varlist[j]);
}
else if (type == ProgressParser.WID_FRAME)
{
addFrame(varlist[j].getName(), ProgressParser.WID_FRAME);
}
else if (type >= ProgressParser.BEGIN_WIDGETS &&
type <= ProgressParser.END_WIDGETS )
{
addWidget(true, varlist[j]);
}
}
}
/**
* Adds a variable name and associated token type to the global scope
* of the variable dictionary. Names in this scope can be hidden by
* duplicate names added to scopes that are closer to the top of the
* stack of scopes.
*
* @param name
* Variable name to add.
* @param tokenType
* Lexer and Parser token type associated with the name.
*/
public void addGlobalVariable(String name, int tokenType)
{
addGlobalVariable(name, tokenType, null);
}
/**
* Adds a variable name and associated token type to the global scope
* of the variable dictionary. Names in this scope can be hidden by
* duplicate names added to scopes that are closer to the top of the
* stack of scopes.
*
* @param name
* Variable name to add.
* @param tokenType
* Lexer and Parser token type associated with the name.
* @param cls
* Fully qualified class name of the object instance represented
* or <code>null</code> if this variable does not represent an
* object instance.
*/
public void addGlobalVariable(String name, int tokenType, String cls)
{
Variable var = new Variable(name, tokenType, cls);
var.setBuiltin(true);
addWrappedWorker(varDict, true, name, var);
}
/**
* Removes any global variable associated with this <code>Keyword</code> from the variable
* dictionary. This is only safe to call BEFORE real parsing begins.
*
* @param word
* <code>Keyword</code> to remove (if it exists).
*/
public void removeGlobalVariable(Keyword word)
{
varDict.deleteSymbol(word.getFullText());
}
/**
* Add a new accum builtin variable, to the {@link #accumDict} dictionary.
*
* @param name
* The variable name.
* @param tokenType
* The variable type.
*/
public void addAccumVariable(String name, int tokenType)
{
if (accumDict.getSymbolAtScope(name, 0) == null)
{
addWrappedWorker(accumDict, false, name, new Variable(name, tokenType, null));
}
}
/**
* Add a new inner-block scope.
*/
public void addInnerBlockScope()
{
accumDict.addScope(null);
}
/**
* Pop the last inner-block scope and copy any definitions into the parent/containing scope.
*/
public void deleteInnerBlockScope()
{
accumDict.deleteScope(true);
}
/**
* Adds a variable name and associated token type to the current scope
* of the variable dictionary. This is the same as the top-most scope.
* Names in this scope can hide duplicate names added to scopes that are
* lower on the stack of scopes.
*
* @param name
* Variable name to add.
* @param tokenType
* Lexer and Parser token type associated with the name.
* @param cls
* Fully qualified class name of the object instance represented
* or <code>null</code> if this variable does not represent an
* object instance.
*
* @return The newly created instance.
*/
public Variable addLocalVariable(String name, int tokenType, String cls)
{
Variable var = null;
if (varDict.getSymbolAtScope(name, 0) == null)
{
var = new Variable(name, tokenType, cls);
addWrappedWorker(varDict, false, name, var);
}
return var;
}
/**
* Adds a menu name and associated token type/options to the menu dictionary
* based on a referenced variable in a <code>LIKE</code> clause.
*
* @param name
* Menu name to add.
* @param ast
* DEFINE_MENU or DEFINE_SUB_MENU AST node.
*/
public void addMenuLike(String name, String oldname, Aast ast)
{
if (ast == null)
{
throw new NullPointerException("Root DEFINE_MENU or DEFINE_SUB_MENU statement is null");
}
int type = lookupMenu(oldname);
Variable var = new Variable(name, type, null);
Variable like = (Variable) lookupWrapped(menuDict, oldname);
Aast fopts = like.getOptions(0);
if (fopts != null)
{
var.copyDefaultOptions(fopts);
Collection<Integer> types;
if (ast.getType() == DEFINE_MENU)
{
types = MENU_CHILDREN_TYPES;
}
else if (ast.getType() == DEFINE_SUB_MENU)
{
types = SUBMENU_CHILDREN_TYPES;
}
else
{
throw new IllegalArgumentException("Unsupported root ast type");
}
// copy children asts.
for (Integer astType : types)
{
Variable.overrideIfNotPresent(fopts, ast, astType);
}
// copy annotations of root ast.
for (Iterator<String> it = fopts.annotationKeys(); it.hasNext(); )
{
String key = it.next();
if (ast.getAnnotation(key) == null)
{
AnnotatedAst anast = (AnnotatedAst)ast;
Object ann = fopts.getAnnotation(key);
if (ann instanceof String)
anast.putAnnotation(key, (String)ann);
if (ann instanceof Double)
anast.putAnnotation(key, (Double)ann);
if (ann instanceof Long)
anast.putAnnotation(key, (Long)ann);
if (ann instanceof Boolean)
anast.putAnnotation(key, (Boolean)ann);
}
}
}
if (menuDict.getSymbolAtScope(name, 0) == null)
{
addWrappedWorker(menuDict, false, name, var);
}
}
/**
* Adds a variable name and associated token type/options to the current
* scope of the variable dictionary based on a referenced variable or
* schema field in a <code>LIKE</code> clause. This is the same as the
* top-most scope. Names in this scope can hide duplicate names added to
* scopes that are lower on the stack of scopes.
*
* @param name
* Variable name to add.
* @param ast
* AST node containing the like clause.
*/
public void addLocalVariableLike(String name, Aast ast)
{
int ltype = ast.getType();
int type = ltype;
String oldname = ast.getText();
int extent = getExtentSubscript(ast);
boolean isFld = ltype > BEGIN_FIELDTYPES && ltype < END_FIELDTYPES;
String cls = null;
// the local variable type must be converted to the proper
// VAR_ variant *if* this was a like FIELD_...
switch (ltype)
{
case FIELD_CHAR:
type = VAR_CHAR;
break;
case FIELD_CLASS:
type = VAR_CLASS;
// class fields must propagate the fully qualified class name
cls = (String) ast.getAnnotation("qualified");
break;
case FIELD_COM_HANDLE:
type = VAR_COM_HANDLE;
break;
case FIELD_DATE:
type = VAR_DATE;
break;
case FIELD_DATETIME:
type = VAR_DATETIME;
break;
case FIELD_DATETIME_TZ:
type = VAR_DATETIME_TZ;
break;
case FIELD_DEC:
type = VAR_DEC;
break;
case FIELD_HANDLE:
type = VAR_HANDLE;
break;
case FIELD_INT:
type = VAR_INT;
break;
case FIELD_INT64:
type = VAR_INT64;
break;
case FIELD_LOGICAL:
type = VAR_LOGICAL;
break;
case FIELD_RAW:
type = VAR_RAW;
break;
case FIELD_RECID:
type = VAR_RECID;
break;
case FIELD_ROWID:
type = VAR_ROWID;
break;
default:
// this MUST be at the end of the list of fields, it will match
// BLOB and CLOB which can't be variable types
if (isFld)
{
String err = String.format("Unexpected field type %s!",
ast.getDescriptiveTokenText());
throw new RuntimeException(err);
}
else
{
// TODO: OM: is this correct when creating LIKE variable (copy its true type)?
type = lookupVariable(oldname);
}
break;
}
// class variables must propagate the fully qualified class name
if (ltype == VAR_CLASS)
cls = (String) ast.getAnnotation("qualified");
Variable var = new Variable(name, type, cls);
Aast fopts = null;
if (isFld)
{
// we must pattern the new variable off a referenced field
// obtain the field options AST from the schema
try
{
SchemaHelper<Aast> fcopy = (SchemaDictionary dict) ->
{
Aast fld = null;
// getFieldCopy() is not safe to call if the field doesn't exist
if (dict.isField(oldname))
{
fld = dict.getFieldCopy(oldname, extent);
}
return fld;
};
Predicate<Aast> notNull = (Aast val) -> { return (val == null); };
fopts = processHierarchy(fcopy, notNull);
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
}
else
{
// we must pattern the variable off another variable
Variable like = (Variable) lookupWrapped(varDict, oldname);
fopts = like.getOptions(extent);
}
if (fopts != null)
{
var.copyDefaultOptions(fopts);
}
if (varDict.getSymbolAtScope(name, 0) == null)
{
addWrappedWorker(varDict, false, name, var);
}
}
/**
* Save off the variable dictionary definitions for the current scope into a cache.
*
* @param name
* The name by which the saved definitions can be restored.
*/
public void saveLocalVariables(String name)
{
varDefCache.put(name.toUpperCase(), varDict.entrySet(0));
}
/**
* Restore any previously cached variable dictionary definitions into the current scope.
* <p>
* Any cached definitions will be removed from the cache.
*
* @param name
* The name by which the saved definitions can be located.
*
* @return <code>true</code> if there were variable definitions restored.
*/
public boolean restoreLocalVariables(String name)
{
boolean nonEmpty = false;
Set<Map.Entry> locals = varDefCache.remove(name.toUpperCase());
if (locals != null)
{
Iterator<Map.Entry> iter = locals.iterator();
while (iter.hasNext())
{
Map.Entry entry = iter.next();
varDict.addEntry(false, entry.getKey(), entry.getValue());
nonEmpty = true;
}
}
return nonEmpty;
}
/**
* Remove any previously cached variable dictionary definitions without restoring them
* to the current scope.
*
* @param name
* The name by which the saved definitions can be located.
*/
public void removeLocalVariables(String name)
{
varDefCache.remove(name.toUpperCase());
}
/**
* Inspect the variable definition and write any non-default options into
* the variable object.
*
* @param name
* The variable name which must have its options set.
* @param ast
* The tree to inspect which defines a Progress variable.
*/
public void setVariableOptions(String name, Aast ast)
{
Variable var = lookupLocalDef(name);
if (var == null || ast == null)
{
// nothing to do
return;
}
// the KW_LIKE should already have been processed by now (in the
// addLocalVariableLike() so that defaults are set from that
// variable/field and then can be overridden by explicit options
// so now we process the explicit overrides
var.setOptions(ast);
}
/**
* Stores all non-standard options as annotations to the AST passed as
* a parameter if this is the original definition, otherwise it stores
* a subset of values, most important of which is the cross-reference
* index to allow the original definition to be easily identified and
* accessed from a reference. If an option has the default value, it is
* not saved as an annotation.
*
* @param name
* The variable name which must have its options annotated.
* @param ast
* The AST to annotate.
* @param original
* <code>true</code> if this is to annotate the AST of the
* original definition, <code>false</code> if the AST is only
* a reference.
*/
public void annotateVariableOptions(String name, Aast ast, boolean original)
{
annotateVariableOptions(null, false, name, ast, original);
}
/**
* Stores all non-standard options as annotations to the AST passed as
* a parameter if this is the original definition, otherwise it stores
* a subset of values, most important of which is the cross-reference
* index to allow the original definition to be easily identified and
* accessed from a reference. If an option has the default value, it is
* not saved as an annotation.
*
* @param cname
* The class in which to lookup the variable or <code>null</code> for the current
* class def.
* @param isStatic
* <code>true</code> to force a static lookup.
* @param name
* The variable name which must have its options annotated.
* @param ast
* The AST to annotate.
* @param original
* <code>true</code> if this is to annotate the AST of the
* original definition, <code>false</code> if the AST is only
* a reference.
*/
public void annotateVariableOptions(String cname,
boolean isStatic,
String name,
Aast ast,
boolean original)
{
java.util.function.Function<String, Variable> lookup = (clsName) ->
{
ClassDefinition cls = null;
int access = KW_PRIVATE;
if (clsName != null)
{
access = calcAccessMode(clsName);
cls = lookupClass(clsName);
}
else
{
cls = getCurrentClassDef();
}
return cls.lookupVariableWrapper(name, access, isStatic);
};
annotateVariableOptions(cname, isStatic, name, ast, original,
lookupLocalDef(name), lookup);
}
/**
* Stores all non-standard options as annotations to the AST passed as
* a parameter if this is the original definition, otherwise it stores
* a subset of values, most important of which is the cross-reference
* index to allow the original definition to be easily identified and
* accessed from a reference. If an option has the default value, it is
* not saved as an annotation.
*
* @param cname
* The class in which to lookup the variable or <code>null</code> for the current
* class def.
* @param isStatic
* <code>true</code> to force a static lookup.
* @param name
* The variable name which must have its options annotated.
* @param ast
* The AST to annotate.
* @param original
* <code>true</code> if this is to annotate the AST of the
* original definition, <code>false</code> if the AST is only
* a reference.
* @param localVar
* The local variable definition.
* @param clsLookup
* IF set, a function to lookup this member in a legacy class definition.
*/
public void annotateVariableOptions(String cname,
boolean isStatic,
String name,
Aast ast,
boolean original,
Variable localVar,
java.util.function.Function<String, Variable> clsLookup)
{
Variable var = null;
ClassDefinition cls = cname == null ? getCurrentClassDef() : lookupClass(cname);
// look for local definitions (not class members) first, but only if we are in the same
// class as being (pre)scanned
ClassDefinition current = getCurrentClassDef();
boolean sameClass = !classLookup &&
cls != null &&
current != null &&
cls.getName().equals(current.getName());
if (sameClass)
{
var = localVar;
}
if (var == null && cls != null)
{
// look for a class member, otherwise
var = clsLookup.apply(cname);
}
if (var == null && !sameClass)
{
// fallback to local variables
var = localVar;
}
// if called from the ProgressExpressionEvaluator (to evaluate an
// expression) the normal variable dictionary processing is bypassed
// we annotate
if (var != null)
{
var.annotateOptions(ast, original, inMethod, this);
}
}
/**
* Creates a new field AST node based on a qualified or unqualified
* field name (must be unambiguous) and sets the token type, text and
* all annotations as needed. This centralizes a service which is
* normally handled by the parser directly. It uses the
* {@link #annotateField} method for the core of the lookup and
* annotation logic.
* <p>
* No table promotion will occur based on this field name, even if it
* is a qualified name.
*
* @param name
* The field name from which to obtain options and type.
*
* @return The created AST node.
*/
public Aast createField(String name)
{
ProgressAst field = new ProgressAst();
field.setText(name);
annotateField(field, null, true, false);
return field;
}
/**
* Stores common data (schemaname, bufname, recordtype) as well as
* all non-standard options as annotations to the AST passed as
* a parameter. If an option has the default value, it is not saved as
* an annotation. Optionally sets the type of the given AST based on
* the field found in the schema dictionary. The field will be looked
* up in the dictionary based on the text of the AST node and there
* must be no ambiguity.
* <p>
* If the field cannot be found due to ambiguity (more than 1 match), the
* method will return with no resulting changes.
* <p>
* If no such field exists, some default annotations will be set to
* "unknown".
*
* @param field
* The AST to annotate.
* @param table
* If the field's table is known, this is the name-node for that table.
* Passing this will narrow the lookup just to that table. Pass <code>null</code>
* to do a normal schema namespace lookup (with all the ambiguity and other
* problems that could occur).
* @param setType
* If <code>true</code>, the AST node will have its type set
* based on the field found in the schema dictionary.
* @param promote
* <code>true</code> to force the promotion (in the schema
* dictionary) of the table if the field's name is qualified.
*/
public void annotateField(Aast field, Object table, boolean setType, boolean promote)
{
annotateField(field, table, setType, promote, false);
}
/**
* Stores common data (schemaname, bufname, recordtype) as well as
* all non-standard options as annotations to the AST passed as
* a parameter. If an option has the default value, it is not saved as
* an annotation. Optionally sets the type of the given AST based on
* the field found in the schema dictionary. The field will be looked
* up in the dictionary based on the text of the AST node and there
* must be no ambiguity.
* <p>
* If the field cannot be found due to ambiguity (more than 1 match), the
* method will return with no resulting changes.
* <p>
* If no such field exists, some default annotations will be set to
* "unknown".
*
* @param field
* The AST to annotate.
* @param table
* If the field's table is known, this is the name-node for that table.
* Passing this will narrow the lookup just to that table. Pass <code>null</code>
* to do a normal schema namespace lookup (with all the ambiguity and other
* problems that could occur).
* @param setType
* If <code>true</code>, the AST node will have its type set
* based on the field found in the schema dictionary.
* @param promote
* <code>true</code> to force the promotion (in the schema
* dictionary) of the table if the field's name is qualified.
* @param preferDefaultBuffer
* <code>true</code> allow unqualified fields to use default buffer.
*/
public void annotateField(Aast field,
Object table,
boolean setType,
boolean promote,
boolean preferDefaultBuffer)
{
String ftext = field.getText();
String fbuffer = null;
if (indexFieldSearch && useIndexFieldSearch && ftext.indexOf('.') == -1)
{
// we know for sure the field is part of the 'indexTable' (see isField(String))
ftext = indexTable + "." + ftext;
fbuffer = indexBuffer;
}
NameNode tableNode = (NameNode) table;
FieldInfo finfo = lookupFieldInfo(tableNode, ftext, preferDefaultBuffer);
Aast fback = finfo.getAst();
// make sure we found the backing field
if (fback != null)
{
if (setType)
{
field.setType(fback.getType());
}
// now store an annotation with the fully qualified name and
// the shorted unabbreviated unique buffer name
String fullname = finfo.getQualified();
String bufname = fbuffer == null ? finfo.getBuffer() : fbuffer;
String dbname = finfo.getDatabase();
// was the original reference qualified?
if (promote && ftext.indexOf('.') != -1)
{
// qualified references cause table promotion
promoteTableName(bufname, false, false);
}
if (fullname != null)
{
field.putAnnotation("schemaname", fullname);
}
if (bufname != null)
{
field.putAnnotation("bufname", bufname);
}
if (dbname != null)
{
field.putAnnotation("dbname", dbname);
}
if (tableNode != null)
{
Aast tableAst = tableNode.getAst();
if (tableAst.getType() == BUFFER)
{
tableAst = tableAst.getParent();
}
Aast index = tableAst.getImmediateChild(INDEX, null);
indexLoop: while (index != null)
{
Aast idxProps = index.getImmediateChild(PROPERTIES, null);
if (idxProps != null && idxProps.getImmediateChild(KW_WORD_IDX, null) != null)
{
Aast prop = index.getImmediateChild(INDEX_FIELD, null);
while (prop != null)
{
if (ftext.equalsIgnoreCase(prop.getText()) ||
ftext.toLowerCase().endsWith("." + prop.getText().toLowerCase()))
{
// found the WORD component
field.putAnnotation("word-indexed", true);
break indexLoop; // skip the other indices, the field is marked from now on
}
prop = index.getImmediateChild(INDEX_FIELD, prop); // next property
}
}
index = tableAst.getImmediateChild(INDEX, index); // process next index
}
}
// store the backing construct (this will be TABLE, TEMP_TABLE or WORK_TABLE)
int backing = finfo.getRecordType();
if (backing != -1)
{
field.putAnnotation("recordtype", Long.valueOf(backing));
}
int newtype = field.getType();
String cls = (newtype == FIELD_CLASS) ? ROOT_OBJ_NAME : null;
// temporary variable object to be our worker to annotate
Variable var = new Variable(ftext, newtype, cls);
// we must pattern the new variable off a referenced field
var.copyDefaultOptions(fback);
var.annotateOptions(field, true, inMethod, this);
// link field with the backing node
Long defidLow = (Long) fback.getAnnotation("fdefid-low");
Long defidHigh = (Long) fback.getAnnotation("fdefid-high");
if (defidLow != null && defidHigh != null)
{
field.putAnnotation("frefid-low", defidLow);
field.putAnnotation("frefid-high", defidHigh);
}
}
else
{
// if the FIELD_ type was set via a hint or some other
// unexpected manner, then this will get triggered...
field.putAnnotation("schemaname", "unknown field");
field.putAnnotation("bufname", "unknown buffer");
field.putAnnotation("recordtype", Long.valueOf(-1));
}
}
/**
* Searches the variable dictionary for a symbol matching the passed
* key or matching a keyword's full text if the symbol matches a language
* keyword. This lookup starts at the top-most scoping level and descends
* through each scope level on the stack until a match is found. The
* token type of the first match found is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
*
* @param actual
* Actual text string to match which may be a language keyword
* abbreviation.
*
* @return The token type that was found or -1 if no match was found.
*/
public int lookupVariable(String actual)
{
int varRef = lookupWrappedType(lookupWrapped(accumDict, actual));
if (varRef == -1)
{
varRef = lookupWrappedType(lookupWrapped(varDict, actual));
}
return varRef;
}
/**
* Searches a dictionary containing {@link TokenDataWrapper} objects for a
* symbol matching the passed key or matching a keyword's full text if the
* symbol matches a language keyword. This lookup starts at the top-most
* scoping level and descends through each scope level on the stack until
* a match is found. The wrapped object of the first match found is
* returned.
*
* @param ssd
* The dictionary to search.
* @param actual
* Actual text string to match which may be a language keyword
* abbreviation.
*
* @return The wrapped object that was found or <code>null</code> if no
* match was found.
*/
public TokenDataWrapper lookupWrapped(ScopedSymbolDictionary ssd, String actual)
{
if (ssd == null)
{
// this can be only for runtime conversion
return null;
}
Keyword kw = lookupKeyword(actual);
TokenDataWrapper wrapper = null;
if (kw != null)
{
// the actual text matches a keyword
if (kw.isReserved())
{
// since this is a reserved keyword, this must be a
// built-in variable/function
wrapper = (TokenDataWrapper) ssd.lookupSymbol(kw.getFullText());
}
else
{
// this is an unreserved keyword, this MAY be a
// built-in variable/function BUT we must allow a user-defined
// name to take precedence (note that if the actual text is
// is the same as the full keyword text, then we may still
// get a match here that is the built-in, assuming no
// user-defined name exists --> that is OK too)
wrapper = (TokenDataWrapper) ssd.lookupSymbol(actual);
// check if there was a match to the actual text
if (wrapper == null)
{
// match the full text of the keyword with the built-in
wrapper = (TokenDataWrapper) ssd.lookupSymbol(kw.getFullText());
}
}
}
if (wrapper == null)
{
// there is no keyword or the keyword is not a built-in var/function,
// so this must be a user-defined name
wrapper = (TokenDataWrapper) ssd.lookupSymbol(actual);
}
return wrapper;
}
/**
* Searches a dictionaries containing {@link TokenDataWrapper} objects for a
* symbol matching the passed key or matching a keyword's full text if the
* symbol matches a language keyword. This lookup starts at the top-most
* scoping level and descends through each scope level on the stack until
* a match is found. The wrapped object of the first match found is
* returned.
*
* @param actual
* Actual text string to match which may be a language keyword
* abbreviation.
* @param ssds
* An array of dictionaries to search.
*
* @return The wrapped object that was found or <code>null</code> if no
* match was found.
*/
public TokenDataWrapper lookupWrapped(String actual, ScopedSymbolDictionary<?> ... ssds)
{
for (ScopedSymbolDictionary<?> ssd : ssds)
{
TokenDataWrapper wrapped = lookupWrapped(ssd, actual);
if (wrapped != null)
{
return wrapped;
}
}
return null;
}
/**
* Adds a user-defined function and its return type to the dictionary
* of functions. This is a flat dictionary (there is no scoping). The
* function name is used as the key and a wrapped {@link Function} object
* is the value. Since the dictionary is flat, all function names must be
* unique.
* <p>
* Due to the Progress forward definition capability, this function only
* adds to the dictionary the first time this method is called for a
* given name.
*
* @param name
* Function name.
* @param tokenType
* The token type of the function's return value as defined by
* the Lexer and Parser.
* @param qname
* Fully qualified class name for functions that return an
* object instance, otherwise <code>null</code>.
*/
public void addFunction(String name, int tokenType, String qname)
{
// a forward declaration may have already occurred for this function
// try a lookup
Function func = (Function) funcDict.lookupSymbol(name);
// is this a new function def OR an override of a builtin function?
if (func == null || func.isBuiltin())
{
func = new Function(name, tokenType, false, qname);
addWrappedWorker(funcDict, false, name, func);
}
}
/**
* Adds a built-in Progress function and its return type to the dictionary
* of functions. This is a flat dictionary (there is no scoping). The
* function name is used as the key and a wrapped {@link Function} object
* is the value. Since the dictionary is flat, all function names must be
* unique.
*
* @param name
* Function name.
* @param tokenType
* The token type of the function's return value as defined by
* the Lexer and Parser.
* @param returnsUnknown
* Specifies if this built-in function can possibly return
* the unknown value.
* @param qname
* Fully qualified class name for functions that return an
* object instance, otherwise <code>null</code>.
*/
public void addBuiltinFunction(String name,
int tokenType,
boolean returnsUnknown,
String qname)
{
Function func = new Function(name, tokenType, true, qname);
func.setReturnsUnknown(returnsUnknown ? Boolean.TRUE : Boolean.FALSE);
addWrappedWorker(funcDict, false, name, func);
}
/**
* Adds a built-in Progress function and its return type to the dictionary
* of functions. This is a flat dictionary (there is no scoping). The
* function name is used as the key and a wrapped {@link Function} object
* is the value. Since the dictionary is flat, all function names must be
* unique.
*
* @param name
* Function name.
* @param tokenType
* The token type of the function's return value as defined by
* the Lexer and Parser.
* @param returnsUnknown
* Specifies if this built-in function can possibly return
* the unknown value.
*/
public void addBuiltinFunction(String name,
int tokenType,
boolean returnsUnknown)
{
addBuiltinFunction(name, tokenType, returnsUnknown, null);
}
/**
* Searches the function dictionary for a symbol matching the passed
* key or matching a keyword's full text if the symbol matches a language
* keyword. This lookup starts at the top-most scoping level and descends
* through each scope level on the stack until a match is found. The
* token type of the first match found is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
*
* @param actual
* Actual text string to match which may be a language keyword
* abbreviation.
*
* @return The token type that was found or -1 if no match was found.
*/
public int lookupFunction(String actual)
{
return lookupWrappedType(lookupWrapped(funcDict, actual));
}
/**
* Searches the function dictionary for a symbol matching the passed
* key or matching a keyword's full text if the symbol matches a language
* keyword. This lookup starts at the top-most scoping level and descends
* through each scope level on the stack until a match is found. The
* definition of the first match found is returned.
*
* @param actual
* Actual text string to match which may be a language keyword
* abbreviation.
*
* @return The function definition or <code>null</code> if no match was
* found.
*/
public Function lookupFunctionDef(String actual)
{
return (Function) lookupWrapped(funcDict, actual);
}
/**
* Removes any function associated with this <code>Keyword</code> from the builtin
* function dictionary. This is only safe to call BEFORE real parsing begins.
*
* @param word
* <code>Keyword</code> to remove (if it exists).
*/
public void removeBuiltinFunction(Keyword word)
{
funcDict.deleteSymbol(word.getFullText());
}
/**
* Stores all non-standard options as annotations to the AST passed as
* a parameter if this is the original definition, otherwise it stores
* a subset of values, most important of which is the cross-reference
* index to allow the original definition to be easily identified and
* accessed from a reference. If an option has the default value, it is
* not saved as an annotation.
*
* @param name
* The function name which must have its options annotated.
* @param ast
* The AST to annotate.
* @param original
* <code>true</code> if this is to annotate the AST of the
* original definition, <code>false</code> if the AST is only
* a reference.
*/
public void annotateFunction(String name, Aast ast, boolean original)
{
Function func = (Function) lookupWrapped(funcDict, name);
// special case: preprocessor expression evaluation can resolve to
// function token types without any function being registered since
// the CallbackResolver registers the function token types inside
// the KeywordDictionary; this means that a null pointer can occur
// here, so we just return without doing any annotations in that
// case
if (func == null)
return;
func.annotateOptions(ast, original, this);
}
/**
* Adds a procedure and a token type to the dictionary of procedures.
* This is a flat dictionary (there is no scoping). The procedure name
* is used as the key and the token type is the value. Since the
* dictionary is flat, all procedure names must be unique.
*
* @param name
* Procedure name.
* @param tokenType
* The procedure's token type as defined by the Lexer and Parser.
*/
public void addProcedure(String name, int tokenType)
{
procDict.addSymbol(false, name, Integer.valueOf(tokenType));
}
/**
* Searches the procedure dictionary for a symbol matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
*
* @param name
* Text string to match.
* @return The procedure's token type that was found or -1 if no match
* was found.
*/
public int lookupProcedure(String name)
{
return lookupWorkerExact(procDict, name);
}
/**
* Adds a class name and a token type to the dictionary of classes.
* This is a flat dictionary (there is no scoping). The class name
* is used as the key and the token type is the value. Since the
* dictionary is flat, all class names must be unique.
*
* @param symbol
* Node defining the fully qualified (including package) class
* name.
* @param inherits
* Node defining the fully qualified name of the superclass or
* <code>null</code> for none.
* @param imple
* Node which has the list of interfaces that are implemented
* or <code>null</code> if none exist.
* @param file
* The filename of the .cls that defined this class.
*/
public void addClass(Aast symbol, Aast inherits, Aast imple, String file)
{
String name = symbol.getText();
WorkArea wa = locate();
if (wa.preScanPass == 0)
{
loadClass(name, true);
}
SearchResult found = resolveClassName(name);
ClassDefinition[] parents = calculateParents(name, inherits, found.dotnet);
ClassDefinition[] ifaces = null;
if (imple != null)
{
ifaces = calculateParents(name, imple, found.dotnet);
}
addClass(name, parents, ifaces, found.builtin, found.dotnet, file);
}
/**
* Adds a class name and a token type to the dictionary of classes.
* This is a flat dictionary (there is no scoping). The class name
* is used as the key and the token type is the value. Since the
* dictionary is flat, all class names must be unique.
*
* @param name
* The fully qualified (including package) class name.
* @param parents
* The superclass or <code>null</code> for none.
* @param ifaces
* The list of interfaces that are implemented or
* <code>null</code> if none exist.
* @param builtin
* <code>true</code> if this definition is built-in to Progress.
* @param dotnet
* <code>true</code> if this definition is a .NET class.
* @param file
* The filename of the .cls that defined this class.
*/
public void addClass(String name,
ClassDefinition[] parents,
ClassDefinition[] ifaces,
boolean builtin,
boolean dotnet,
String file)
{
WorkArea wa = locate();
ClassDefinition cls = lookupClass(name);
boolean loaded = (cls != null);// && (!isPreScan() || !wa.preScanRefs.contains(name)));
if (!loaded)
{
cls = new ClassDefinition(name, parents, ifaces, builtin, OOType.CLASS, dotnet, file);
}
while (parents != null)
{
SchemaWorker.loadFromPersistence(schemaDict, parents[0].getFilename());
// classes can only inherit from 1 parent and it must also be a class
// so we can hard code to the first array element
parents = parents[0].getParents();
}
// remember this is the class definition being updated (with methods,
// members and properties)
currentCls.push(cls);
// add the default constructor
addObjectMethod(cls.getSimpleName(), OO_METH_VOID, KW_PUBLIC, false, null, null);
if (loaded)
{
return;
}
classDict.addSymbol(false, name, cls);
wa.preScanRefs.add(name);
}
/**
* Adds an interface name and a token type to the dictionary of classes.
* This is a flat dictionary (there is no scoping). The interface name
* is used as the key and the token type is the value. Since the
* dictionary is flat, all interface names must be unique.
*
* @param symbol
* Node defining the fully qualified (including package) interface
* name.
* @param inherits
* Optional inherits clause.
* @param file
* The filename of the .cls that defined this interface.
*/
public void addInterface(Aast symbol, Aast inherits, String file)
{
String name = symbol.getText();
SearchResult found = resolveClassName(name);
// all interfaces inherit methods from Progress.Lang.Object
ClassDefinition[] parents = calculateParents(name, inherits, found.dotnet);
addInterface(name, parents, found.builtin, found.dotnet, file);
}
/**
* Adds an interface name and a token type to the dictionary of classes.
* This is a flat dictionary (there is no scoping). The interface name
* is used as the key and the token type is the value. Since the
* dictionary is flat, all interface names must be unique.
*
* @param name
* The fully qualified (including package) interface name.
* @param parents
* Optional parent class definition(s) for an inherits clause.
* @param builtin
* <code>true</code> if this definition is built-in to Progress.
* @param dotnet
* <code>true</code> if this definition is a .NET class.
* @param file
* The filename of the .cls that defined this interface.
*/
public void addInterface(String name,
ClassDefinition[] parents,
boolean builtin,
boolean dotnet,
String file)
{
WorkArea wa = locate();
if (wa.preScanPass == 0)
{
loadClass(name, true);
}
ClassDefinition iface = lookupClass(name);
boolean loaded = (iface != null);
if (!loaded)
{
iface = new ClassDefinition(name,
parents,
null,
builtin,
OOType.INTERFACE,
dotnet,
file);
}
// remember this is the interface definition being updated (with
// methods, members and properties)
currentCls.push(iface);
if (loaded)
{
return;
}
classDict.addSymbol(false, name, iface);
wa.preScanRefs.add(name);
}
/**
* Adds an enum definition to the dictionary of classes.
*
* @param symbol
* Node defining the fully qualified (including package) enum
* name.
* @param flags
* {@code true} if this is a flags enum.
* @param file
* The filename of the .cls that defined this interface.
*/
public void addEnum(Aast symbol, boolean flags, String file)
{
String name = symbol.getText();
SearchResult found = resolveClassName(name);
WorkArea wa = locate();
if (wa.preScanPass == 0)
{
loadClass(name, true);
}
ClassDefinition edef = lookupClass(name);
boolean loaded = (edef != null);
if (!loaded)
{
// ensure progress.lang.object is loaded before loading the enum class (as in FWD, this inherits
// from PLO, and will trigger a circular dependency
loadClass(ROOT_OBJ_NAME);
String pname = found.dotnet
? "System.Enum"
: flags ? "Progress.Lang.FlagsEnum" : "Progress.Lang.Enum";
// regular enums use parent Progress.Lang.Enum, flag enums use Progress.Lang.FlagsEnum
loadClass(pname);
ClassDefinition parent = lookupClass(pname);
if (parent == null)
{
if (wa.preScanRefs.contains(pname))
{
String err = String.format("Parent class already in pre-scan - postponed: %s!", pname);
throw new ParentUnavailableException(err);
}
String err = String.format("Missing enum parent class definition for %s!", pname);
throw new RuntimeException(err);
}
ClassDefinition[] pars = new ClassDefinition[] { parent };
edef = new ClassDefinition(name,
pars,
null,
found.builtin,
OOType.ENUM,
found.dotnet,
file);
// the 4GL has an undocumented member of all .NET enum instances that returns the integer value of
// that instance; this implicit member does NOT exist for 4GL enums; references will appear in the
// source code in this syntax: System.Windows.Forms.TabAlignment:TOP:value__
if (edef.isDotNet())
{
Variable value__ = new Variable("value__", VAR_INT, null);
addVariable(true, value__);
edef.addVariable(DEFINE_ENUM, "value__", value__, null, KW_PUBLIC, false, null, false);
value__.setAnnotationHelper((Variable var, Aast ast, boolean ori, boolean meth, SymbolResolver sym) ->
{
ast.putAnnotation("dotnet-enum-implicit-value", true);
});
}
}
// remember this is the enum definition being updated (with members)
currentCls.push(edef);
if (loaded)
{
return;
}
classDict.addSymbol(false, name, edef);
wa.preScanRefs.add(name);
}
/**
* Adds a member to the current enum definition.
*
* @param symbol
* Node defining the new enum member.
*/
public void addEnumMember(Aast symbol)
{
ClassDefinition cls = getCurrentClassDef();
if (cls != null)
{
String name = symbol.getText();
String qname = cls.getName();
Variable var = addLocalVariable(name, VAR_CLASS, qname);
// we mark this as a non-static var so that annotations will not mark it as static
var.setProperty(false);
var.setStatic(false);
var.annotateOptions(symbol, true, false, this);
// we register it as static because it is implicitly static in its lookup
cls.addVariable(DEFINE_ENUM, name, var, symbol, KW_PUBLIC, true, qname, false);
}
}
/**
* Obtain the currently active class or interface definition.
*
* @return The active class or interface definition or <code>null</code>
* if there is no active definition.
*/
public ClassDefinition getCurrentClassDef()
{
return currentCls == null || currentCls.empty() ? null : currentCls.peek();
}
/**
* Obtain the name of the currently active class definition.
*
* @return The class name or <code>null</code> if there is no active
* class definition.
*/
public String getCurrentClassName()
{
ClassDefinition cls = getCurrentClassDef();
return (cls == null) ? null : cls.getName();
}
/**
* Obtain the parent of the currently active class definition.
*
* @return The parent class definition or <code>null</code> if there is
* no explicit parent.
*/
public ClassDefinition[] getCurrentParentsDef()
{
ClassDefinition cls = getCurrentClassDef();
return (cls == null) ? null : cls.getParents();
}
/**
* Obtain the name of the parent of the currently active class definition. This can only
* return a single name and MUST NEVER be called for an interface since interfaces can
* have multiple inheritance.
*
* @return The parent class name.
*/
public String getCurrentParentName()
{
ClassDefinition[] cls = getCurrentParentsDef();
return (cls == null) ? ROOT_OBJ_NAME : cls[0].getName();
}
/**
* Notify the symbol resolver that the current class or interface
* definition is complete.
*/
public void classDefinitionComplete()
{
currentCls.pop();
}
/**
* Attempt to load the class or interface name given. This "eats" any
* failures since it is not guaranteed that the input does represent a
* valid class name.
*
* @param name
* A potential class or interface name.
*
* @return The fully qualified class name if it can be loaded or <code>null</code> if the
* class or interface cannot be loaded.
*/
public String tryLoadClass(String name)
{
String full = null;
WorkArea wa = locate();
// do not interfere with the currently processed classes - the hierarchy will be processed when the
// 'caller' finishes.
List<String> save = new ArrayList<>();
save.addAll(wa.processed);
try
{
wa.processed.clear();
// if the class can't be loaded, this will force throwing an exception
wa.forceLoadFailure = true;
full = loadClass(name);
}
catch (Throwable exp)
{
// TODO: uncomment after TransformDriver supports setting up of cmd line logging level
//LOG.log(Level.FINE, "FAILURE in tryLoadClass()", exp);
}
finally
{
wa.forceLoadFailure = false;
wa.processed.clear();
wa.processed.addAll(save);
}
return full;
}
/**
* Attempt to load the class or interface name given. This "eats" any
* failures since it is not guaranteed that the input does represent a
* valid class name.
*
* @param name
* A potential class or interface name.
*
* @return <code>true</code> if the class or interface can be loaded.
*/
public boolean canLoadClass(String name)
{
return (tryLoadClass(name) != null);
}
/**
* Load (using recursion if needed) the hierarchy of class definitions (or
* a single interface definition) referenced by the given name. This should
* be called when a reference to a class or interface name occurs (except
* in the case of the <code>CLASS</code> or <code>INTERFACE</code> language
* statements). The name can be unqualified (requiring a call to
* {@link #resolveClassName} to determine the fully qualified name) or it
* can be either partially or fully qualified (partially qualified names were
* added in v11). Once resolved, if the class or interface definition
* is already loaded, then this will return immediately. Otherwise the
* class or interface file will be found via the <code>PROPATH</code> and
* that file's AST will be loaded. If the AST does not exist, then the
* file will first be parsed. During parsing, the <code>CLASS</code> or
* <code>INTERFACE</code> language statements will call {@link #addClass}
* or {@link #addInterface} respectively. This will instantiate the proper
* {@link ClassDefinition} objects and the contained definitions for
* methods, member variables and properties will be added by the rules
* that match those language features. Once parsing is complete, the
* definition will be fully formed and loaded. Any references to other
* classes or interfaces in that file will cause recursive parsing/loading
* using this method, until the entire object hierarchy is loaded. At that
* point, this chain of methods will return.
*
* @param name
* Unqualified or partially/fully qualified class or interface name to
* load.
*
* @return The fully qualified class or interface name that was loaded.
*/
public String loadClass(String name)
{
return loadClass(name, false);
}
/**
* Load (using recursion if needed) the hierarchy of class definitions (or
* a single interface definition) referenced by the given name. This should
* be called when a reference to a class or interface name occurs (except
* in the case of the <code>CLASS</code> or <code>INTERFACE</code> language
* statements). The name can be unqualified (requiring a call to
* {@link #resolveClassName} to determine the fully qualified name) or it
* can be either partially or fully qualified (partially qualified names were
* added in v11). Once resolved, if the class or interface definition
* is already loaded, then this will return immediately. Otherwise the
* class or interface file will be found via the <code>PROPATH</code> and
* that file's AST will be loaded. If the AST does not exist, then the
* file will first be parsed. During parsing, the <code>CLASS</code> or
* <code>INTERFACE</code> language statements will call {@link #addClass}
* or {@link #addInterface} respectively. This will instantiate the proper
* {@link ClassDefinition} objects and the contained definitions for
* methods, member variables and properties will be added by the rules
* that match those language features. Once parsing is complete, the
* definition will be fully formed and loaded. Any references to other
* classes or interfaces in that file will cause recursive parsing/loading
* using this method, until the entire object hierarchy is loaded. At that
* point, this chain of methods will return.
*
* @param name
* Unqualified or partially/fully qualified class or interface name to
* load.
* @param topLevel
* {@code true} if the named class is already being parsed in the caller.
*
* @return The fully qualified class or interface name that was loaded.
*/
public String loadClass(String name, boolean topLevel)
{
SearchResult found = resolveClassName(name);
ClassDefinition cls = lookupClass(found.clsname);
WorkArea wa = locate();
if (cls == null)
{
cls = loadClassDefinition(this, found.clsname);
if (cls != null)
{
return found.clsname;
}
if (found.java)
{
if (loadJavaClass(found.clsname) == null)
{
return null;
}
return found.clsname;
}
if (wa.preScanRefs.contains(found.clsname))
{
// in process of being loaded
return found.clsname;
}
// not loaded yet
// do we have a cached file name?
if (found.filename == null)
{
// find the class file name in the propath
if (!srchPropathFindFile(found.clsname, found.type, found))
{
if (wa.preScanPass >= 1 && !wa.forceLoadFailure)
{
classDict.addSymbol(false, found.clsname, MOCK_CLASS_DEF);
wa.preScanRefs.add(name);
// this class is not found, but it must not break parsing - show a warning and
// 'sink' all references to it
String err = reportClassNotFound(found.clsname, false);
System.out.printf("WARNING: Lvl%02d parse: %s\n", wa.preScanPass, err);
return found.clsname;
}
else
{
// we fail only if we are referencing this class from the currently parsed
// file (we are not in pre-scan-mode)
reportClassNotFound(found.clsname, true);
}
}
// at this point found.filename must be valid
}
if (generator == null)
{
generator = new AstGenerator();
generator.setSilent(silent);
generator.setPreprocess(true);
generator.setCache(true);
generator.setDumpLexer(true);
generator.setDumpParser(true);
generator.setAstPersist(true);
}
String relative = Configuration.normalizeFilename(found.filename);
if (wa.preScanPass == 0)
{
wa.preScanRefs.clear();
}
wa.preScanRefs.add(found.clsname);
wa.preScanPass++;
boolean inPreScanParent = false;
try
{
if (!silent)
{
String str = String.join("|", Collections.nCopies(wa.preScanPass, " "));
System.out.printf(" Lvl%02d parse:%s%s\n",
wa.preScanPass,
str,
relative);
}
// the PROPATH is inherited from the caller
generator.preScanClass(relative, found.builtin, found.dotnet, this.propath);
// save all referenced classes/interfaces except for the genesis object
if (wa.preScanPass > 1)
{
wa.processed.add(found.clsname);
}
}
catch (ParentUnavailableException exc)
{
System.err.println("Postpone loading class from " + name + ": " + exc.getMessage());
inPreScanParent = true;
}
catch (AstException |
ANTLRException |
IOException e)
{
// if we reach here, do not abort. 4GL is lenient if there are parse errors in
// classes referenced via i.e. var types; what matters is that the file can be found,
// but it doesn't matter if it can compile or not. Only explicit compile of that
// class will report it.
// although FWD attempts to fully compile the class, this is just a warning that there
// might be problems in the application's legacy 4GL code, which the user needs to
// fix/address. In any case, if we reach this step, explicit compile of this class
// will always fail in both 4GL and FWD, and we are fine with it.
String msg = String.format("Failed Lvl%02d parse of %s: %s",
wa.preScanPass,
found.clsname,
e.getMessage());
LOG.log(Level.WARNING, msg);
LOG.log(Level.WARNING, "", e);
}
finally
{
wa.preScanPass--;
if (wa.preScanPass == 0)
{
wa.preScanRefs.clear();
System.out.println(" Lvl01 DONE: " + relative);
}
}
if (!inPreScanParent)
{
cls = lookupClass(found.clsname);
if (cls == null)
{
String spec = "Cannot load class/interface %s from file %s!";
String err = String.format(spec, found.clsname, relative);
throw new RuntimeException(err);
}
if (!isPreScan())
{
// we cannot just process the genesis object that started the graph; we must process all
// referenced classes/interfaces including those not in our parent hierarchy AND we need
// to process them in the same order they were processed previously
while (!wa.processed.isEmpty())
{
String fname = wa.processed.remove();
// ensure all super-classes are loaded for this object
parseHierarchy(fname, wa);
}
// do NOT handle the 2nd level parsing of the genesis object here if that parsing is already
// happening in the caller
if (!topLevel)
{
parseHierarchy(cls.getName(), wa);
}
}
}
}
return found.clsname;
}
/**
* Searches the class dictionary for a definition matching the passed key.
*
* @param name
* Fully qualified class/interface name to match.
*
* @return The class that was found or <code>null</code> if no match was found.
*/
public ClassDefinition lookupClass(String name)
{
if (classDict == null)
{
return null;
}
ClassDefinition cls = null;
cls = (ClassDefinition) classDict.lookupSymbol(name);
if (cls == null)
{
cls = locate().classCache.get(name.toLowerCase());
}
if (cls != null && cls.isMock() && locate().preScanPass == 0)
{
// in parsing (non-prescan) mode, if we are resolving the MOCK_CLASS_DEF, then do not
// allow it as the name is really missing.
cls = null;
}
return cls;
}
/**
* Annotate a published class event, with the details of the parameter signature.
*
* @param evt
* The event node, annotated with its class definition.
* @param call
* The event publish node.
* @param isStatic
* <code>true</code> if the call is from a static context.
*/
public void annotateObjectEvent(Aast evt, Aast call, boolean isStatic)
{
String cname = (String) evt.getAnnotation("found-in-cls");
ClassDefinition cls = lookupClass(cname);
cls.annotateObjectEvent(evt, call, isStatic);
}
/**
* Annotate the given method invocation reference with details for the specified method
* in this class. This is a no-operation if the method doesn't exist or if the node is
* <code>null</code>.
*
* @param cname
* Class name, may be <code>null</code>.
* @param mname
* Method name, <code>null</code> is a c'tor invocation.
* @param isStatic
* <code>true</code> if the method is static.
* @param node
* The AST to be annotated.
*/
public void annotateObjectMethod(String cname, String mname, boolean isStatic, Aast node)
{
annotateObjectMethod(cname, mname, isStatic, false, node);
}
/**
* Annotate the given method invocation reference with details for the specified method
* in this class. This is a no-operation if the method doesn't exist or if the node is
* <code>null</code>.
*
* @param cname
* Class name, may be <code>null</code>.
* @param mname
* Method name, <code>null</code> is a c'tor invocation.
* @param isStatic
* <code>true</code> if the method is static.
* @param isSuper
* <code>true</code> if the rvalue refnode is the SUPER keyword.
* @param node
* The AST to be annotated.
*/
public void annotateObjectMethod(String cname, String mname, boolean isStatic, boolean isSuper, Aast node)
{
ClassDefinition current = getCurrentClassDef();
ClassDefinition cls = (cname == null) ? current : lookupClass(cname);
boolean internal = (cls == current);
int access = calcAccessMode(cname);
if (cls != null)
{
ParameterKey[] sig = calculateCallSignature(node);
try
{
cls.annotateMethodCall(mname, sig, access, isStatic, isSuper, internal, node);
}
catch (Throwable t)
{
throw new RuntimeException("Could not annotate method call at\n" + node.dumpTree(true), t);
}
if (node.getType() == OO_METH_CLASS)
{
String rcls = (String) node.getAnnotation("qualified");
// this leaves different annotations than above; the annotations are about
// the returned reference NOT about the called method
if (rcls == null)
{
System.out.println("WARNING: No annotateClassRef due to missing 'qualified' annotation\n" + node.dumpTree(true));
}
else
{
annotateClassRef(rcls, node);
}
}
}
}
/**
* Annotate the given node which is a reference to the named class.
*
* @param qname
* Fully qualified class name being referenced.
* @param node
* The node to annotate.
*/
public void annotateClassRef(String qname, Aast node)
{
// Note that this should no longer happen, since we have added checks prior to invoking
// annotateClassRef, but left here in case there are other situations added.
if (qname == null)
{
System.out.println("FAILURE: Returning from annotateClassRef because of null qname " +
node.dumpTree(true));
return;
}
ClassDefinition cls = lookupClass(qname);
if (cls == null)
{
// try to load the class
qname = loadClass(qname);
cls = lookupClass(qname);
if (cls == null)
{
WorkArea wa = locate();
if (wa.preScanRefs.contains(qname) && wa.preScanPass > 0)
{
// the pre-scan is still in progress, do not annotate
// except for one important annotation that is utilized
// downstream during pre-scan itself for class name
// "lookahead", for lack of a better term.
// WARNING: since we don't have a class definition, we are
// making the assumption that this is not a Java class and
// converting to lowercase
node.putAnnotation("qualified", qname.toLowerCase());
return;
}
else
{
reportClassNotFound(qname, true);
}
}
}
node.putAnnotation("qualified", cls.isJava() ? qname : qname.toLowerCase());
node.putAnnotation("source-file", cls.getFilename());
node.putAnnotation("builtin-cls", cls.isBuiltIn());
node.putAnnotation("dotnet-cls", cls.isDotNet());
node.putAnnotation("indirect-dotnet", cls.isIndirectDotNetReference());
node.putAnnotation("is-class", cls.isClass());
node.putAnnotation("is-interface", cls.isInterface());
node.putAnnotation("is-enum", cls.isEnum());
node.putAnnotation("is-java", cls.isJava());
if (cls.isDotNet() && node.getText().endsWith("]"))
{
node.putAnnotation("dotnet-array", true);
}
ClassDefinition current = getCurrentClassDef();
if (current != null && current != cls)
{
current.markDotNetUsage(cls);
}
}
/**
* Converts any unqualified or partially qualified class or interface names into the fully
* qualified name (which has a package included). Before v11 there was no such thing as a
* partially qualified class or interface name in Progress, but now they exists and are
* supported here. When any "." character is found in the source name, that name is assumed
* be qualified in some manner (fully or partially).
* <p>
* The following name resolution actions are attempted:
* <p>
* <ol>
* <li> If the name matches the unqualified name of any of the
* explicitly imported classes (via the USING statement), that
* fully qualified class name is returned.
* <li> The "*" in each of the imported package specifications is
* replaced with the unqualified name (in order) and:
* <ol>
* <li> If the resulting name matches a class that is already loaded,
* that class name is set into the search result, but the other
* search result members will NOT be set.
* <li> The following paths are searched depending on the USING type:
* <ul>
* <li> <code>PROPATH</code> for the project's 4GL cls files
* <li> <code>OO4GL_PATH</code> for built-in 4GL cls files
* <li> <code>ASSEMBLY_PATH</code> for the project's .NET cls
* files
* <li> <code>DOTNET_PATH</code> for built-in .NET cls files
* </ul>
* <li> If a match is found via the search, the members of the search
* result will be set as noted below.
* </ol>
* </ol>
* <p>
* If an unqualified name is not matched to the above criteria, the name
* is assumed to be the complete type name and is returned unchanged. This
* means that no search has been successful.
* <p>
* If the filesystem search has been successful, the returned search
* result will have a non-null <code>filename</code> member. In addition,
* the <code>builtin</code> and <code>dotnet</code> members will have been
* set and can be relied upon.
*
* @param uname
* Unqualified or fully qualified class or interface name.
*
* @return An object containing data about the search. If the
* <code>filename</code> member is <code>null</code> then only
* the <code>name</code> member has been set, though it may not
* be valid. If the <code>filename</code> member is NOT
* <code>null</code>, then all members of the object are already
* set and can be relied upon.
*/
public SearchResult resolveClassName(String uname)
{
return resolveClassName(uname, UsingType.UNSPECIFIED);
}
/**
* Converts any unqualified or partially qualified class or interface names into the fully
* qualified name (which has a package included). Before v11 there was no such thing as a
* partially qualified class or interface name in Progress, but now they exists and are
* supported here. When any "." character is found in the source name, that name is assumed
* be qualified in some manner (fully or partially).
* <p>
* The following name resolution actions are attempted:
* <p>
* <ol>
* <li> If the name matches the unqualified name of any of the
* explicitly imported classes (via the USING statement), that
* fully qualified class name is returned.
* <li> The "*" in each of the imported package specifications is
* replaced with the unqualified name (in order) and:
* <ol>
* <li> If the resulting name matches a class that is already loaded,
* that class name is set into the search result, but the other
* search result members will NOT be set.
* <li> The following paths are searched depending on the USING type:
* <ul>
* <li> <code>PROPATH</code> for the project's 4GL cls files
* <li> <code>OO4GL_PATH</code> for built-in 4GL cls files
* <li> <code>ASSEMBLY_PATH</code> for the project's .NET cls
* files
* <li> <code>DOTNET_PATH</code> for built-in .NET cls files
* </ul>
* <li> If a match is found via the search, the members of the search
* result will be set as noted below.
* </ol>
* </ol>
* <p>
* If an unqualified name is not matched to the above criteria, the name
* is assumed to be the complete type name and is returned unchanged. This
* means that no search has been successful.
* <p>
* If the filesystem search has been successful, the returned search
* result will have a non-null <code>filename</code> member. In addition,
* the <code>builtin</code> and <code>dotnet</code> members will have been
* set and can be relied upon.
*
* @param uname
* Unqualified or fully qualified class or interface name.
* @param utype
* The type of tje class to search.
*
* @return An object containing data about the search. If the
* <code>filename</code> member is <code>null</code> then only
* the <code>name</code> member has been set, though it may not
* be valid. If the <code>filename</code> member is NOT
* <code>null</code>, then all members of the object are already
* set and can be relied upon.
*/
public SearchResult resolveClassName(String uname, UsingType utype)
{
String[] qualified = new String[1];
UsingSpec[] uspec = new UsingSpec[1];
SearchResult result = new SearchResult();
// remove array brackets if they exist (this assumes they are always
// at the end AND that there is no other valid content after the
// opening square bracket); note that brackets can define multi-
// dimensional arrays like MyClass[,,] for a 3-dimensional array
int sqBrackets = uname.indexOf("[");
String name = sqBrackets >= 0 ? uname.substring(0, sqBrackets) : uname;
// lambda for searching the imported packages (USING blah.blah.*)
Runnable pkgLookup = () ->
{
Iterator<UsingSpec> iter = searchPkgDict.iterator();
// try each of the search specifications in order
while (iter.hasNext() && qualified[0] == null)
{
uspec[0] = iter.next();
// build the next possible qualified class name
String next = String.format("%s%s", uspec[0].spec, name);
// short circuit the search logic in the case that we already
// have loaded this class definition
ClassDefinition cls = lookupClass(next);
if (cls != null)
{
qualified[0] = next;
result.builtin = cls.isBuiltIn();
result.dotnet = cls.isDotNet();
result.java = cls.isJava();
}
else
{
// search propath for the specific class file name
if (findFile(next, uspec[0].type, result))
{
qualified[0] = next;
}
}
}
};
if (name.indexOf('.') == -1)
{
// check the explicit list of class names
uspec[0] = explPkgDict.get(name.toLowerCase());
qualified[0] = (uspec[0] != null) ? uspec[0].spec : null;
if (qualified[0] == null)
{
// fallback to search imported packages
pkgLookup.run();
}
if (qualified[0] == null)
{
// the unqualified name must be the full name
qualified[0] = name;
}
}
else
{
// given the assumption that a basepath.something.qualifier.Gadget.cls class exists,
// the following is allowed:
//
// USING basepath.something.*.
// DEFINE VARIABLE ref AS qualifier.Gadget.
//
// This partially qualified case is resolved by:
//
// 1. check if the name exists as-is
// 2. if not, do a search as above using searchPkgDict
if (findFile(name, utype, result))
{
// fully qualified, found
qualified[0] = name;
if (utype != UsingType.UNSPECIFIED)
{
result.type = utype;
result.java = (utype == UsingType.JAVA);
result.dotnet = (utype == UsingType.DOTNET);
}
}
else
{
// fallback to search imported packages
pkgLookup.run();
if (qualified[0] == null)
{
// last chance, this may be a fully qualified Java class reference that has no
// associated USING statement so there was no previous way to look it up
if (isJavaClass(name, result))
{
// the edits have already been made to the result structure
return result;
}
// not found, fallback to the name
qualified[0] = name;
}
}
}
if (uspec[0] != null)
{
utype = uspec[0].type;
result.type = utype;
result.java = (utype == UsingType.JAVA);
result.dotnet = (utype == UsingType.DOTNET);
}
result.clsname = qualified[0];
return result;
}
/**
* Adds a package search specification ("package.*") or a fully qualified
* class name ("package.class") to the list of known packages. These are
* used to match unqualified class names with their fully qualified
* equivalents.
*
* @param filename
* The file where this USING statement appears.
* @param using
* The USING node.
* @param name
* Package search specification or explicit fully qualified
* class name.
* @param fromClause
* The nodes parsed as the <code>FROM (ASSEMBLY | PROPATH | JAVA)</code>
* clause or <code>null</code> if it wasn't present. A Java package will
* only be processed if explicitly specified (non-null FROM JAVA clause).
*/
public void addPackage(String filename, Aast using, String name, Aast fromClause)
{
UsingType type = UsingType.UNSPECIFIED;
if (fromClause != null)
{
Aast child = fromClause.getChildAt(0);
if (child != null)
{
switch (child.getType())
{
case KW_ASSEMBLY:
type = UsingType.DOTNET;
break;
case KW_PROPATH:
type = UsingType.PROPATH;
break;
case KW_JAVA:
type = UsingType.JAVA;
break;
default:
LOG.log(Level.SEVERE, String.format("Unrecognized FROM %s!", child.getText()));
}
}
}
if (name.endsWith(".*"))
{
// remove the asterisk since we will no longer need it (it only
// serves to denote that this is a search spec versus an explicit
// fully qualified class name
name = name.substring(0, name.length() - 1);
// one cannot easily check for Java package existence; using
// Package.getPackage(name) or Package.getPackages() only return data for
// packages in which a class was already loaded; we can't assume that is the
// case here; using ClassLoader.getResource() or ClassLoader.getResources()
// need specific resource names (instead of a package name); instead as long as
// the programmer has specfied Java we must assume it is
// if not explicitly Java, then assume it is 4GL or .NET
if (type != UsingType.JAVA)
{
// error out if the directory can't be found in the PROPATH, this does not check Java
String path = pkgExists(name);
if (path == null)
{
// 4GL doesn't abend if a package (with asterisk) doesn't exist.
LOG.log(Level.SEVERE, String.format("Non-existent 4GL or .NET package %s!", name));
using.setHidden(true);
return;
}
else
{
if (path.startsWith("." + File.separator))
{
path = path.substring(2);
}
path = path.replace(File.separatorChar, '.');
if ((caseSens && !path.equals(name)) || (!caseSens && !path.equalsIgnoreCase(name)))
{
using.putAnnotation("normalized-path", path);
}
}
}
searchPkgDict.add(new UsingSpec(name, type));
}
else
{
// find the base name
int idx = name.lastIndexOf(".");
if (idx < 0)
{
throw new RuntimeException("Invalid fully qualified class name!");
}
String base = name.substring(idx + 1).toLowerCase();
// if more than one USING clause has the same base name, the 4GL only honors the first
if (!explPkgDict.containsKey(base))
{
// map the lowercased base name to the fully qualified name (allows
// case-insensitive lookup)
explPkgDict.put(base, new UsingSpec(name, type));
}
// prescan this class (if needed) and save its source file; this needs to be done only
// if we are not 'using' the same class as the current class being defined
SearchResult found = resolveClassName(name, type);
String relative = (found == null || found.filename == null || found.java)
? null
: Configuration.normalizeFilename(found.filename);
WorkArea wa = locate();
// take a peek at the preScanRefs searching a class we are already parsing
if ((relative == null || !filename.equalsIgnoreCase(relative)) &&
!(wa.preScanRefs.contains(found.clsname)))
{
if (name == null)
{
System.out.println("\nWARNING: unable to annotate Class Ref (classname==null)");
}
else
{
annotateClassRef(name, using);
}
}
else
{
using.putAnnotation("qualified", found.java ? name : name.toLowerCase());
using.putAnnotation("source-file", filename);
using.putAnnotation("builtin-cls", found.builtin);
using.putAnnotation("dotnet-cls", found.dotnet);
using.putAnnotation("is-java", found.java);
}
}
}
/**
* Add the named method to the currently active class definition.
*
* @param name
* Method name.
* @param mtype
* Return type for the method.
* @param access
* Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
* or <code>KW_PRIVATE</code>).
* @param isStatic
* <code>true</code> if the variable/property is static.
* @param node
* The AST to be annotated.
* @param ret
* The AST for the method's return type. This is used to copy OO annotations back to
* the method node.
*/
public void addObjectMethod(String name,
int mtype,
int access,
boolean isStatic,
Aast node,
Aast ret)
{
// must be non-null since methods can't be defined outside of classes
getCurrentClassDef().addMethod(name, mtype, access, isStatic, node, ret);
}
/**
* Add the named variable, property or class event as a data member of the currently active
* class definition. If no class definition is active, this method does nothing.
*
* @param stype
* Defining language statement type.
* @param name
* Member name.
* @param node
* The definition node for this member.
* @param access
* Node of type <code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
* or <code>KW_PRIVATE</code>.
* @param stic
* Node of type <code>KW_STATIC</code> if the variable/property is
* static.
* @param ov
* Override node if specified.
* @param ab
* Abstract node if specified.
* @param qname
* Fully qualified class name for members that are references to a given class
* or <code>null</code> if this member is not an object instance.
* @param extent
* The extent value or 0 if there is no extent. The AST cannot be inspected at this point
* because all the child notes are not linked into the root node yet. For this reason, the
* explicit value must be passed.
*/
public void addDataMember(int stype,
String name,
Aast node,
Aast access,
Aast stic,
Aast ov,
Aast ab,
String qname,
int extent)
{
// this is called both in pre-scan and normal parse phases:
// 1. in pre-scan, it will pickup and collect the DEF VAR scoped with methods/c'tor/etc
// 2. in normal parse, it will not pickup these (as the body will be ignored)
ClassDefinition cls = getCurrentClassDef();
if (cls != null)
{
// we are deliberately bypassing keyword lookup that happens in lookupWrapped(); otherwise it
// conflicts with using reserved keywords as class data member names
Variable var = (Variable) varDict.lookupSymbol(name);
// properties and class events default to public, def var defaults to private
int dmode = (stype == DEFINE_VARIABLE) ? KW_PRIVATE : KW_PUBLIC;
int amode = (access == null) ? dmode : access.getType();
boolean isStatic = (stic != null);
boolean isProp = (stype == DEFINE_PROPERTY);
// if the previous definition is a property, and we are in pre-scan mode, then ignore it; this is
// just a dirty fix for pre-scan mode, it's not complete
if (var.isProp() && !isProp && isPreScan())
{
return;
}
var.setProperty(isProp);
var.setStatic(isStatic);
var.setExtent(extent);
var.setOverride(ov != null);
var.setAbstract(ab != null);
// check if any interface parents has this property defined, as re-defining it as abstract or
// implementing it in a sub-class doesn't require the OVERRIDE option
if (var.isProp() && !var.isOverride())
{
Variable found = cls.lookupVariableWrapper(var.getName(), KW_PROTECTD, var.isStatic());
if (found != null)
{
var.setOverride(found.isOverride() || found.getClassDefinition() != var.getClassDefinition());
}
}
trackPropertyOrEventMethodSignatures(stype, cls, node, var, name);
// a variable is provisional during prescan, if and only if we have no explicit access modifier set
boolean provisional = isPreScan() && stype == DEFINE_VARIABLE && access == null;
cls.addVariable(stype, name, var, node, amode, isStatic, qname, provisional);
}
}
/**
* Set the proper flags at the property's {@link Variable} instance, specifying if the getter or setter
* are defined at this property definition.
*
* @param name
* The property name.
* @param getter
* <code>true</code> if a getter is defined.
* @param setter
* <code>true</code> if a setter is defined.
*/
public void postProcessProperty(String name, boolean getter, boolean setter)
{
ClassDefinition cls = getCurrentClassDef();
if (cls != null)
{
Variable var = (Variable) lookupWrapped(varDict, name);
var.setWithGetter(getter);
var.setWithSetter(setter);
}
}
/**
* Check if the given class is built-in.
*
* @param name
* The qualified class name.
*
* @return See above.
*/
public boolean isBuiltInClass(String name)
{
SearchResult found = resolveClassName(name);
return found != null && found.builtin;
}
/**
* Check if the given name is for a builtin function.
*
* @param name
* The function's name.
*
* @return See above.
*/
public boolean isBuiltinFunction(String name)
{
Function def = lookupFunctionDef(name);
return def != null && def.isBuiltin();
}
/**
* Check if the given class is a .NET class.
*
* @param name
* The qualified class name.
*
* @return See above.
*/
public boolean isDotNetClass(String name)
{
SearchResult found = resolveClassName(name);
return found != null && found.dotnet;
}
/**
* Reports if the given data member is a static member of the given class.
*
* @param cname
* Class name to search within or <code>null</code> if this is
* a member within the local class definition.
* @param name
* Member name to lookup.
*
* @return <code>true</code> if the member is static. <code>false</code> otherwise
* including the case where the member does not exist at all.
*/
public boolean isStaticDataMember(String cname, String name)
{
ClassDefinition cls = (cname == null) ? getCurrentClassDef() : lookupClass(cname);
return cls != null && cls.isStaticDataMember(name, calcAccessMode(cname));
}
/**
* Searches the named class' hierarchy for the named data member. This
* will match both static and instance members.
*
* @param cname
* Class name to search within or <code>null</code> if this is
* a member within the local class definition.
* @param name
* Member name to lookup.
*
* @return The token type of the member or -1 if no match was found.
*/
public int lookupDataMember(String cname, String name)
{
int type = lookupDataMember(cname, name, false);
return type == -1 ? lookupDataMember(cname, name, true) : type;
}
/**
* Searches the named class' hierarchy for the named data member.
*
* @param cname
* Class name to search within or <code>null</code> if this is
* a member within the local class definition.
* @param name
* Member name to lookup.
* @param isStatic
* If <code>true</code>, only static methods will be looked up.
* Otherwise any method can be returned.
*
* @return The token type of the member or -1 if no match was found.
*/
public int lookupDataMember(String cname, String name, boolean isStatic)
{
return lookupDataMemberWorker(cname, name, isStatic);
}
/**
* Searches the named class' hierarchy for the named data member and
* returns the fully qualified class name if that member represents an
* object instance.
*
* @param cname
* Class name to search within or <code>null</code> if this is
* a member within the local class definition.
* @param name
* Member name to lookup.
* @param isStatic
* <code>true</code> if this is a static reference.
*
* @return Fully qualified class name of the the object instance
* represented by the named variable, or <code>null</code> if no
* such variable exists.
*/
public String lookupDataMemberClass(String cname, String name, boolean isStatic)
{
ClassDefinition cls = (cname == null) ? getCurrentClassDef() : lookupClass(cname);
int access = calcAccessMode(cname);
return (cls == null) ? null : cls.lookupVariableClassName(name, access, isStatic);
}
/**
* Searches the named class' hierarchy for the named method. This will match any method
* (static or instance) of the given name.
*
* @param cname
* Class name to search within or <code>null</code> if this is a method call
* within the local class definition.
* @param method
* Method name to lookup.
* @param isStatic
* If <code>true</code>, only static methods will be looked up. Otherwise any
* method can be returned.
*
* @return {@code true} if a match was found.
*/
public boolean isObjectMethod(String cname, String method, boolean isStatic)
{
return lookupObjectMethod(cname, method, isStatic) != -1;
}
/**
* Searches the named class' hierarchy for the named method.
* <p>
* <b>If a positive value is returned, the caller can be assured that there is an accessible
* method with that name BUT the actual type of the return value cannot be known until the
* signature is known. This is due to the fact that the same method name can be present with
* differing return values and only the parameter signature matching can differentiate the
* exact method that will be called.</b>
* <p>
* <b>Do not call this from any location where the type must be known authoritatively.</b>
*
* @param cname
* Class name to search within or <code>null</code> if this is
* a method call within the local class definition.
* @param method
* Method name to lookup.
* @param isStatic
* If <code>true</code>, only static methods will be looked up.
* Otherwise any method can be returned.
*
* @return The return type of the method or -1 if no match was found.
*/
public int lookupObjectMethod(String cname, String method, boolean isStatic)
{
ClassDefinition current = getCurrentClassDef();
ClassDefinition cls = (cname == null) ? current : lookupClass(cname);
boolean internal = (cls == current);
int access = calcAccessMode(cname);
return (cls == null) ? -1 : cls.guessMethodType(method, access, isStatic, internal);
}
/**
* Add all specified keyword token types to the correct attribute and method type mappings.
*
* @param attrsAndMethods
* The attribute/method mappings.
*/
public void addAllAttributesAndMethods(Map<Integer, Integer> attrsAndMethods)
{
if (attributes != null)
{
attributes.putAll(attrsAndMethods);
}
}
/**
* Set the attribute or method's OO type, in the {@link #attrTypes} map.
*
* @param ktype
* Keyword token type to be used as the key in the dictionary.
* @param cls
* Fully qualified name of the class (user defined type) that
* is returned from this method. Must be <code>null</code> if
* the resultType is not <code>ATTR_CLASS</code> or
* <code>METH_CLASS</code>.
*/
public void setAttributeOrMethodType(int ktype, String cls)
{
if (cls != null && attrTypes != null)
{
attrTypes.put(ktype, cls);
}
}
/**
* Adds an attribute or method keyword token type and the associated token
* type of the data type (for attributes) or return type (for methods) to
* the dictionary of attributes/methods.
* <p>
* This is a flat dictionary (there is no scoping). The keyword's token
* type is used as the key and the result token type is the value.
* <p>
* This form of the method is NOT suitable for adding attributes or
* methods with <code>ATTR_CLASS</code> or <code>METH_CLASS</code> result
* types. This is due to the fact that there is no mechanism to
* associate a class name with the resulting attribute or method.
*
* @param ktype
* Keyword token type to be used as the key in the dictionary.
* @param rtype
* The token type representing the attribute data type or the
* method return type as defined by the Lexer and Parser.
*/
public void addAttributeOrMethod(int ktype, int rtype)
{
addAttributeOrMethod(ktype, rtype, null);
}
/**
* Adds an attribute or method keyword token type and the associated token
* type of the data type (for attributes) or return type (for methods) to
* the dictionary of attributes/methods.
* <p>
* This is a flat dictionary (there is no scoping). The keyword's token
* type is used as the key and the result token type is the value.
*
* @param ktype
* Keyword token type to be used as the key in the dictionary.
* @param rtype
* The token type representing the attribute data type or the
* method return type as defined by the Lexer and Parser.
* @param cls
* Fully qualified name of the class (user defined type) that
* is returned from this method. Must be <code>null</code> if
* the resultType is not <code>ATTR_CLASS</code> or
* <code>METH_CLASS</code>.
*/
public void addAttributeOrMethod(int ktype, int rtype, String cls)
{
attributes.put(ktype, rtype);
if (cls != null)
attrTypes.put(ktype, cls);
}
/**
* Searches the attribute and method dictionary for a token type matching
* the passed keyword. The token type of the method return type or the
* attribute data type is returned, if a match is found. A token type
* of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
*
* @param ktype
* Token type of the keyword to lookup.
*
* @return The attribute or method token type that was found or -1 if no
* match was found.
*/
public int lookupAttributeOrMethod(int ktype)
{
Integer result = attributes.get(ktype);
return result == null ? -1 : result.intValue();
}
/**
* Searches the attribute and method dictionary for the fully qualified
* class name that is the actual type of an <code>ATTR_CLASS</code> or
* <code>METH_CLASS</code> matching the given keyword.
*
* @param ktype
* Token type of the keyword to lookup.
*
* @return The fully qualified class name that is the actual type
* of an <code>ATTR_CLASS</code> or <code>METH_CLASS</code>.
* <code>null</code> if the given keyword is not a valid class
* attribute or method.
*/
public String lookupAttributeOrMethodClass(int ktype)
{
int ttype = lookupAttributeOrMethod(ktype);
String result = null;
if (ttype == ATTR_CLASS || ttype == METH_CLASS)
{
result = attrTypes.get(ktype);
}
return result;
}
/**
* Adds a label and a token type to the dictionary of labels. This is
* a special form of a scoped dictionary where there is only one label name
* per scope (Progress does not allow lable aliases). The label name is
* used as the key and the token type is the value.
*
* @param name
* Label name.
* @param tokenType
* The label's token type as defined by the Lexer and Parser.
*/
public void addLabel(String name, int tokenType)
{
labelDict.addScope(null);
labelDict.addSymbol(false, name, Integer.valueOf(tokenType));
}
/**
* Searches the label dictionary for a symbol matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
*
* @param name
* Text string to match.
* @return The label's token type that was found or -1 if no match was
* found.
*/
public int lookupLabel(String name)
{
return lookupWorkerExact(labelDict, name);
}
/**
* Deletes the current label from the dictionary of labels. This is
* a special form of a scoped dictionary where there is only one label name
* per scope (Progress does not allow lable aliases). So, deleting the
* current label is deleting the current scope.
*/
public void deleteLabel()
{
labelDict.deleteScope();
}
/**
* Adds a widget and a token type to the dictionary of widgets.
* This dictionary supports scoping that matches the variable namespace.
* The widget name is used as the key and the token type is the value.
* <p>
* Note that this is artificially separated from the variable namespace
* for implementation simplicity, but in Progress there is only one
* namespace that handles both variables and widgets.
*
* @param name
* Widget name.
* @param tokenType
* The procedure's token type as defined by the Lexer and Parser.
*/
public void addWidget(String name, int tokenType)
{
Variable var = new Variable(name, tokenType, null);
addWrappedWorker(widgetDict, false, name, var);
}
/**
* Searches the widget dictionary for a symbol matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
* <p>
* Note that this is artificially separated from the variable namespace
* for implementation simplicity, but in Progress there is only one
* namespace that handles both variables and widgets.
*
* @param name
* Text string to match.
* @return The widget's token type that was found or -1 if no match
* was found.
*/
public int lookupWidget(String name)
{
Variable var = lookupLocalDef(name);
if (var != null)
{
int ttype = var.getTokenType();
// any lookup for a variable which can not be a possible widget will return 'no match'
switch (ttype)
{
case VAR_RAW:
case VAR_MEMPTR:
case VAR_CLASS:
case VAR_HANDLE:
case VAR_COM_HANDLE:
return -1;
}
}
return lookupWrappedType(lookupWrapped(widgetDict, name));
}
/**
* Adds a frame and a token type to the dictionary of frames. The frame
* name is used as the key and the token type is the value.
*
* @param name
* Frame name.
* @param tokenType
* The token type as defined by the Lexer and Parser.
*/
public void addFrame(String name, int tokenType)
{
frameDict.addSymbol(false, name, Integer.valueOf(tokenType));
}
/**
* Get the set of field reference ASTs associated with a particular frame.
*
* @param frame
* Name of the frame for which field references should be retrieved.
*
* @return Set of field references associated with the given frame.
*/
public Set<Aast> getFrameFields(String frame)
{
Set<Aast> fieldSet = fieldWidgetDict.lookup(frame);
if (fieldSet == null)
{
fieldSet = new HashSet<>();
fieldWidgetDict.addSymbol(false, frame, fieldSet);
}
return fieldSet;
}
/**
* Map a collection of field reference ASTs to the name of the frame in which they reside as
* widgets.
*
* @param frame
* Name of the frame contaning the field widgets.
* @param fields
* Collection of field ASTs to be stored.
*/
public void addFrameFields(String frame, Collection<Aast> fields)
{
Set<Aast> fieldSet = getFrameFields(frame);
for (Aast fld : fields)
{
String key1 = fld.getAnnotation("schemaname") + "$" + fld.getAnnotation("bufname");
boolean found = false;
// a field is unique if schemaName$bufname are the same...
for (Aast sfld : fieldSet)
{
String key2 = sfld.getAnnotation("schemaname") + "$" + sfld.getAnnotation("bufname");
if (key1.equals(key2))
{
found = true;
break;
}
}
if (!found)
{
fieldSet.add(fld);
}
}
}
/**
* Map a field reference ASTs to the name of the frame in which it resides as a widget.
*
* @param frame
* Name of the frame contaning the field widgets.
* @param field
* Field AST to be stored.
*/
public void addFrameField(String frame, Aast field)
{
addFrameFields(frame, Collections.singleton(field));
}
/**
* Check if the USE-DICT-EXPS option is active for the given frame.
*
* @param frame
* The frame name.
*
* @return <code>true</code> if USE-DICT-EXPS is active.
*/
public boolean isUseDictExps(String frame)
{
Boolean state = useDictExps.lookup(frame);
return state != null && state;
}
/**
* Set the USE-DICT-EXPS for this frame, if not already set.
*
* @param frame
* The frame name.
*/
public void markUseDictExps(String frame)
{
Boolean state = useDictExps.lookup(frame);
if (state == null)
{
useDictExps.addSymbol(false, frame, true);
}
}
/**
* Searches the frame dictionary for a symbol matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
*
* @param name
* Text string to match.
* @return The frame's token type that was found or -1 if no match
* was found.
*/
public int lookupFrame(String name)
{
return lookupWorkerExact(frameDict, name);
}
/**
* Adds a menu or sub-menu and a token type to the dictionary of menus.
* The menu or sub-menu name is used as the key and the token type is the
* value.
*
* @param name
* Menu or sub-menu name.
* @param tokenType
* The token type as defined by the Lexer and Parser.
*/
public void addMenu(String name, int tokenType)
{
Variable var = new Variable(name, tokenType, null);
addWrappedWorker(menuDict, false, name, var);
}
/**
* Searches the menu dictionary for a symbol matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
* <p>
* This supports both menus and sub-menus.
*
* @param name
* Text string to match.
* @return The menu's token type that was found or -1 if no match
* was found.
*/
public int lookupMenu(String name)
{
return lookupWrappedType(lookupWrapped(menuDict, name));
}
/**
* Adds a menu-item and a token type to the dictionary of menu-items.
* The menu-item name is used as the key and the token type is the value.
*
* @param name
* Menu-item name.
* @param tokenType
* The token type as defined by the Lexer and Parser.
*/
public void addMenuItem(String name, int tokenType)
{
Variable var = new Variable(name, tokenType, null);
addWrappedWorker(menuItemDict, false, name, var);
}
/**
* Searches the menu-item dictionary for a symbol matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
*
* @param name
* Text string to match.
* @return The menu-item's token type that was found or -1 if no match
* was found.
*/
public int lookupMenuItem(String name)
{
return lookupWrappedType(lookupWrapped(menuItemDict, name));
}
/**
* Adds a stream and a token type to the dictionary of streams.
* The stream name is used as the key and the token type is the value.
*
* @param name
* Stream name.
* @param tokenType
* The token type as defined by the Lexer and Parser.
*/
public void addStream(String name, int tokenType)
{
streamDict.addSymbol(false, name, tokenType);
}
/**
* Searches the stream dictionary for a symbol matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
*
* @param name
* Text string to match.
* @return The stream's token type that was found or -1 if no match
* was found.
*/
public int lookupStream(String name)
{
return lookupWorkerExact(streamDict, name);
}
/**
* Adds a query and a token type to the dictionary of queries.
* The query name is used as the key and the token type is the value.
*
* @param name
* Query name.
* @param node
* The definition node for this member.
* @param tokenType
* The token type as defined by the Lexer and Parser.
* @param am
* Access modifiers if not null. Only will be non-null if this is a resource
* defined as a member of a class definition.
* @param st
* Static specifier if not null. Only will be non-null if this is a resource
* defined as a member of a class definition.
*/
public void addQuery(String name, Aast node, int tokenType, Aast am, Aast st)
{
qryDict.addSymbol(false, name, tokenType);
ClassDefinition def = getCurrentClassDef();
if (def != null)
{
def.addQuery(name, node, tokenType, decodeAccessMode(am), decodeStatic(st), inMethod);
}
}
/**
* Searches the query dictionary for a symbol matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
*
* @param name
* Text string to match.
* @return The query's token type that was found or -1 if no match
* was found.
*/
public int lookupQuery(String name)
{
int result = lookupWorkerExact(qryDict, name);
if (result == -1)
{
ClassDefinition[] parents = getCurrentParentsDef();
if (parents != null)
{
for (ClassDefinition par : parents)
{
// TODO: we need to know the context of the calling code here to know whether
// this is a static or instance lookup; for now we try both
result = par.lookupQuery(name, KW_PROTECTD, false);
if (result == -1)
{
result = par.lookupQuery(name, KW_PROTECTD, true);
}
if (result != -1)
break;
}
}
}
return result;
}
/**
* Adds a dataset and a token type to the dictionary of datasets. The dataset name is used as
* the key and the token type is the value.
*
* @param name
* Dataset name.
* @param tokenType
* The token type as defined by the Lexer and Parser.
* @param node
* The definition AST.
* @param am
* Access modifiers if not null. Only will be non-null if this is a resource
* defined as a member of a class definition.
* @param st
* Static specifier if not null. Only will be non-null if this is a resource
* defined as a member of a class definition.
*/
public void addDataSet(String name, int tokenType, Aast node, Aast am, Aast st)
{
ClassDefinition def = getCurrentClassDef();
boolean isStatic = decodeStatic(st);
Variable dataset = createVariable(name, tokenType, node, isStatic);
if (def != null)
{
def.addDataSet(dataset, node, name, tokenType, decodeAccessMode(am), isStatic, inMethod);
}
addWrappedWorker(dsetDict, false, name, dataset);
}
/**
* Searches the dataset dictionary for a symbol matching the passed key. The token type of the
* match is returned. Note that a token type of -1 is invalid in the Lexer and Parser, so this
* value is used to denote a search that found no match.
*
* @param name
* Text string to match.
*
* @return The dataset's token type that was found or -1 if no match was found.
*/
public int lookupDataSet(String name)
{
int result = -1;
Variable ds = (Variable) dsetDict.lookupSymbol(name);
if (ds != null)
{
result = ds.getTokenType();
}
else
{
ClassDefinition[] parents = getCurrentParentsDef();
if (parents != null)
{
for (ClassDefinition par : parents)
{
// TODO: we need to know the context of the calling code here to know whether
// this is a static or instance lookup; for now we try both
result = par.lookupDataSet(name, KW_PROTECTD, false);
if (result == -1)
{
result = par.lookupDataSet(name, KW_PROTECTD, true);
}
if (result != -1)
break;
}
}
}
return result;
}
/**
* Annotates a DATASET or DATA-SOURCE node with the information needed to relate the static
* defined variable references to their definitions.
*
* @param cname
* The class in which to lookup the dataset or <code>null</code> for the current
* class def.
* @param isStatic
* Flag indicating that we are in a static context.
* @param dsname
* The legacy dataset name.
* @param root
* The Ast node that will be annotated.
* @param type
* The type of the token. May be DATASET or DATA-SOURCE when processing a reference
* but also DEFINE_DATASET or DEFINE_DATA_SOURCE when processing their definitions.
* @param original
* Flag indicating this is the original definition.
*/
public void setDatasetOptions(String cname,
boolean isStatic,
String dsname,
Aast root,
long type,
boolean original)
{
Variable ds = null;
if (type == DEFINE_DATASET || type == DATA_SET)
{
ds = (Variable) dsetDict.lookupSymbol(dsname);
root.putAnnotation("type", (long) DATA_SET);
}
else if (type == DEFINE_DATA_SOURCE || type == DATA_SOURCE)
{
ds = (Variable) dsrcDict.lookupSymbol(dsname);
root.putAnnotation("type", (long) DATA_SOURCE);
}
java.util.function.Function<String, Variable> lookup = (clsName) ->
{
ClassDefinition cls = null;
int access = KW_PRIVATE;
if (clsName != null)
{
access = calcAccessMode(clsName);
cls = lookupClass(clsName);
}
else
{
cls = getCurrentClassDef();
}
if (type == DEFINE_DATASET || type == DATA_SET)
{
return cls.lookupDataSetWrapper(dsname, access, isStatic);
}
else if (type == DEFINE_DATA_SOURCE || type == DATA_SOURCE)
{
return cls.lookupDataSourceWrapper(dsname, access, isStatic);
}
return null;
};
annotateVariableOptions(cname, isStatic, dsname, root, original, ds, lookup);
}
/**
* Adds a data-source and a token type to the dictionary of data-sources.
* The data-source name is used as the key and the token type is the value.
*
* @param name
* Data-source name.
* @param tokenType
* The token type as defined by the Lexer and Parser.
* @param node
* The definition AST.
* @param am
* Access modifiers if not null. Only will be non-null if this is a resource
* defined as a member of a class definition.
* @param st
* Static specifier if not null. Only will be non-null if this is a resource
* defined as a member of a class definition.
*/
public void addDataSource(String name, int tokenType, Aast node, Aast am, Aast st)
{
ClassDefinition def = getCurrentClassDef();
boolean isStatic = decodeStatic(st);
Variable ds = createVariable(name, tokenType, node, isStatic);
if (def != null)
{
def.addDataSource(ds, node, name, tokenType, decodeAccessMode(am), isStatic, inMethod);
}
addWrappedWorker(dsrcDict, false, name, ds);
}
/**
* Searches the data-source dictionary for a symbol matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
*
* @param name
* Text string to match.
* @return The data-source's token type that was found or -1 if no match
* was found.
*/
public int lookupDataSource(String name)
{
int result = -1;
Variable ds = (Variable) dsrcDict.lookupSymbol(name);
if (ds != null)
{
result = ds.getTokenType();
}
else
{
ClassDefinition[] parents = getCurrentParentsDef();
if (parents != null)
{
for (ClassDefinition par : parents)
{
// TODO: we need to know the context of the calling code here to know whether
// this is a static or instance lookup; for now we try both
result = par.lookupDataSource(name, KW_PROTECTD, false);
if (result == -1)
{
result = par.lookupDataSource(name, KW_PROTECTD, true);
}
if (result != -1)
break;
}
}
}
return result;
}
/**
* Adds a data-relation and a token type to the dictionary of data-relations. The data-relation
* name is used as the key and the token type is the value.
*
* @param name
* Data-relation name.
* @param tokenType
* The token type as defined by the Lexer and Parser.
*/
public void addDataRelation(String name, int tokenType)
{
drelDict.addSymbol(false, name, tokenType);
}
/**
* Searches the data-relation dictionary for a symbol matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match.
*
* @param name
* Text string to match.
* @return The data-relation's token type that was found or -1 if no match
* was found.
*/
public int lookupDataRelation(String name)
{
return lookupWorkerExact(drelDict, name);
}
/**
* Loads a set of databases into the schema dictionary. This is the way that non-default databases are
* 'connected' or added into the known namespace.
*
* @param dblist
* The {@code Set} of database names to load.
*/
public void loadSchemaDatabases(Set<String> dblist)
{
if (dblist == null || schemaDict == null)
{
return;
}
try
{
for (String db : dblist)
{
schemaDict.loadSchema(db);
}
}
catch (SchemaException excpt)
{
LOG.log(Level.WARNING, "", excpt);
}
}
/**
* Adds a list of aliases (each to an associated database name) to the
* schema dictionary.
*
* @param list
* A list of string arrays, each representing an alias and
* associated database to add to the schema dictionary.
*/
public void loadAliases(List<String[]> list)
{
if (schemaDict != null)
{
int len = list.size();
for (int i = 0; i < len; i++)
{
String[] pair = list.get(i);
boolean result = schemaDict.createAlias(pair[0], pair[1]);
if (!result)
{
// todo: yes, more logging here too
LOG.log(Level.WARNING, "createAlias() failed on " + pair[0] + " and database " + pair[1]);
}
}
}
}
/**
* Returns the shortest unique non-abbreviated buffer name for a field
* or table reference. This allows one to calculate the identical buffer
* name from each reference, such that all references that use the same
* buffer will share the same buffer name, even if the references are
* different (due to abbreviations, field vs. table, qualified vs
* unqualified) in the original source code.
*
* @param name
* Text string to match, exactly as found in the original source
* code (abbreviated, qualified or inqualified).
* @param field
* <code>true</code> if this is a field reference,
* <code>false</code> if this is a table reference.
*
* @return The shortest unabbreviated unique buffer name or
* <code>null</code> if no match was found.
*/
public String lookupBufferName(String name, boolean field)
{
return lookupBufferName(name, field, false, false);
}
/**
* Returns the shortest unique non-abbreviated buffer name for a field
* or table reference. This allows one to calculate the identical buffer
* name from each reference, such that all references that use the same
* buffer will share the same buffer name, even if the references are
* different (due to abbreviations, field vs. table, qualified vs
* unqualified) in the original source code.
* <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
* Text string to match, exactly as found in the original source
* code (abbreviated, qualified or inqualified).
* @param field
* <code>true</code> if this is a field reference,
* <code>false</code> if this is a table reference.
* @param forceTemp
* If <code>true</code>, force a temp-table lookup.
* @param forcePersistent
* If <code>true</code>, force a persistent table lookup.
*
* @return The shortest unabbreviated unique buffer name or
* <code>null</code> if no match was found.
*/
public String lookupBufferName(String name, boolean field, boolean forceTemp, boolean forcePersistent)
{
String result = null;
try
{
if (schemaDict != null)
{
SchemaHelper<String> bff = (SchemaDictionary dict) ->
{
return dict.getBufferForField(name, forceTemp, forcePersistent);
};
SchemaHelper<String> bft = (SchemaDictionary dict) ->
{
return dict.getBufferForTable(name, forceTemp, forcePersistent);
};
Predicate<String> notNull = (String val) -> { return (val == null); };
result = processHierarchy(field ? bff : bft, notNull);
}
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return result;
}
/**
* Returns the logical database name being used for a given table
* reference. The resulting name may be different from the database
* portion of the fully qualified schema name as returned by the
* {@link #lookupFullTableName} since the same schema can be referenced
* as multiple database instances.
*
* @param name
* Text string to match, exactly as found in the original source
* code (abbreviated, qualified or inqualified).
*
* @return The logical database name or <code>null</code> if no match was
* found.
*/
public String lookupDatabaseName(String name)
{
return lookupDatabaseName(name, false, false);
}
/**
* Returns the logical database name being used for a given table
* reference. The resulting name may be different from the database
* portion of the fully qualified schema name as returned by the
* {@link #lookupFullTableName} since the same schema can be referenced
* as multiple database instances.
* <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
* Text string to match, exactly as found in the original source
* code (abbreviated, qualified or inqualified).
* @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 database name or <code>null</code> if no match was
* found.
*/
public String lookupDatabaseName(String name, boolean forceTemp, boolean forcePersistent)
{
String result = null;
try
{
if (schemaDict != null)
{
SchemaHelper<String> ldft = (SchemaDictionary dict) ->
{
String dbname = null;
// getLogicalDatabaseForTable() assumes that the node will be found, so we can
// only call it if we know the node exists in the current dictionary
if (dict.isTable(name, forceTemp, forcePersistent))
{
dbname = dict.getLogicalDatabaseForTable(name, forceTemp, forcePersistent);
}
return dbname;
};
Predicate<String> notNull = (String val) -> { return (val == null); };
result = processHierarchy(ldft, notNull);
}
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return result;
}
/**
* Returns the fully qualified name for a field matching the passed
* key.
* <p>
* Schema field names can come in 3 forms:
* <ul>
* <li> unqualified (e.g. address1)
* <li> partially qualified with a tablename (e.g. customer.address1)
* <li> fully qualified with database and table (e.g.
* salesapp.customer.address1)
* </ul>
* <p>
* All 3 forms are accepted as input. All searches are done on
* a case-insensitive basis. In addition, the Progress 4GL schema
* supports optional abbreviation of field name portion. These
* abbreviations are valid as long as they represent a unique match. The
* smallest unique match to a table name is the smallest possible
* abbreviation. As such, no ambiguity is allowed in the name passed as
* an argument. There must be only one match that is ever found in the
* schema namespace.
*
* @param name
* Text string to match, in either of the 3 possible forms.
*
* @return The field's fully qualified schema name or <code>null</code>
* if no match was found.
*/
public String lookupFullFieldName(String name)
{
String fqn = null;
try
{
if (schemaDict != null)
{
SchemaHelper<String> ffn =
(SchemaDictionary dict) -> { return dict.getFullFieldName(name); };
Predicate<String> notNull = (String val) -> { return (val == null); };
fqn = processHierarchy(ffn, notNull);
}
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return fqn;
}
/**
* Searches the schema dictionary for a field matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match, or one that failed due to an
* ambiguous field name or due to some other problem with the schema
* dictionary.
* <p>
* Schema field names can come in 3 forms:
* <ul>
* <li> unqualified (e.g. address1)
* <li> partially qualified with a tablename (e.g. customer.address1)
* <li> fully qualified with database and table (e.g.
* salesapp.customer.address1)
* </ul>
* <p>
* All 3 forms are accepted as input. All searches are done on
* a case-insensitive basis. In addition, the Progress 4GL schema
* supports optional abbreviation of tablenames and fieldnames. These
* abbreviations are valid as long as they represent a unique match. The
* smallest unique match to a table or field name is the smallest
* possible abbreviation. As such, no ambiguity is allowed in the
* name passed as an argument. There must be only one match that is
* ever found in the schema namespace.
*
* @param table
* The table being processed or {@code null} if no specific table is being
* processed.
* @param name
* Text string to match, in any of the supported 3 forms.
*
* @return The label's token type that was found or -1 if no match was
* found.
*/
public int lookupField(NameNode table, String name)
{
int type = -1;
try
{
if (schemaDict != null)
{
SchemaHelper<Integer> fdt = (SchemaDictionary dict) ->
{
int ftype = -1;
String fldName = name;
if (indexFieldSearch && useIndexFieldSearch && name.indexOf(".") == -1)
{
fldName = String.format("%s.%s", indexTable, name);
if (!dict.isField(fldName))
{
// fallback to the original name and disable index matching
fldName = name;
endIndexFieldSearch();
}
}
ftype = dict.getFieldDataType(table, fldName);
return ftype;
};
Predicate<Integer> notNull =
(Integer val) -> { return (val == null || val.intValue() == -1) ; };
type = processHierarchy(fdt, notNull);
}
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return type;
}
/**
* Searches the schema dictionary for a field matching the passed
* key and returns a boolean reporting if it is a valid field or not.
* <p>
* Schema field names can come in 3 forms:
* <ul>
* <li> unqualified (e.g. address1)
* <li> partially qualified with a tablename (e.g. customer.address1)
* <li> fully qualified with database and table (e.g.
* salesapp.customer.address1)
* </ul>
* <p>
* All 3 forms are accepted as input. All searches are done on
* a case-insensitive basis. In addition, the Progress 4GL schema
* supports optional abbreviation of tablenames and fieldnames. These
* abbreviations are valid as long as they represent a unique match. The
* smallest unique match to a table or field name is the smallest
* possible abbreviation. As such, no ambiguity is allowed in the
* name passed as an argument. There must be only one match that is
* ever found in the schema namespace.
* <p>
* <b>This method is safe to use inside a semantic predicate since it
* will not throw an exception if the name is not found or if there is
* ambiguity detected!</b>
*
* @param name
* Text string to match, in any of the supported 3 forms.
*
* @return true if the name is a valid field, false otherwise.
*/
public boolean isField(String name)
{
boolean result = false;
if (schemaDict != null)
{
try
{
SchemaHelper<Boolean> isFld = (SchemaDictionary dict) ->
{
String fldName = name;
if (indexFieldSearch && useIndexFieldSearch && name.indexOf('.') == -1)
{
fldName = String.format("%s.%s", indexTable, name);
if (dict.isField(fldName))
{
return true;
}
else
{
// fallback to the original name and disable index matching
fldName = name;
endIndexFieldSearch();
}
}
return dict.isField(fldName);
};
Predicate<Boolean> whileFalse = (Boolean val) -> { return (val == Boolean.FALSE); };
result = processHierarchy(isFld, whileFalse);
}
catch (SchemaException exc)
{
// this should not happen
LOG.log(Level.SEVERE, "", exc);
}
}
return result;
}
/**
* Searches the schema dictionary for a field matching the passed
* key and returns a boolean reporting if it is a valid field or not.
* Exact name matching version of <code>isField</code>
* @param name
* Text string to match, in any of the supported 3 forms, exact name match required
*
* @return true if the name is a valid field, false otherwise.
*/
public boolean isFieldExactName(String name)
{
boolean result = false;
if (schemaDict != null)
{
try
{
SchemaHelper<Boolean> isFld = (SchemaDictionary dict) ->
{
String fldName = name;
if (indexFieldSearch && useIndexFieldSearch && name.indexOf('.') == -1)
{
fldName = String.format("%s.%s", indexTable, name);
if (dict.isExactMatchField(fldName) )
{
return true;
}
else
{
// fallback to the original name and disable index matching
fldName = name;
endIndexFieldSearch();
}
}
return dict.isExactMatchField(fldName);
};
Predicate<Boolean> whileFalse = (Boolean val) -> { return (val == Boolean.FALSE); };
result = processHierarchy(isFld, whileFalse);
}
catch (SchemaException exc)
{
// this should not happen
LOG.log(Level.SEVERE, "", exc);
}
}
return result;
}
/**
* Returns the fully qualified name for a table matching the passed
* key.
* <p>
* Schema table names can come in 2 forms:
* <ul>
* <li> unqualified, table name only (e.g. customer)
* <li> fully qualified with database and table (e.g. salesapp.customer)
* </ul>
* <p>
* Both forms of input are accepted. All searches are done on
* a case-insensitive basis. In addition, the Progress 4GL schema
* supports optional abbreviation of tablenames portion. These
* abbreviations are valid as long as they represent a unique match. The
* smallest unique match to a table name is the smallest possible
* abbreviation. As such, no ambiguity is allowed in the name passed as
* an argument. There must be only one match that is ever found in the
* schema namespace.
* <p>
* In addition to the predefined table names from the schema, the user
* may define or create buffers, temp-tables and work-tables that all
* are interchangeable with table names. This represents a dynamic
* table namespace which must appear as a single unified space.
*
* @param name
* Text string to match, in either of the 2 possible forms.
*
* @return The table's fully qualified schema name or <code>null</code>
* if no match was found.
*/
public String lookupFullTableName(String name)
{
return lookupFullTableName(name, false, false);
}
/**
* Returns the fully qualified name for a table matching the passed
* key.
* <p>
* Schema table names can come in 2 forms:
* <ul>
* <li> unqualified, table name only (e.g. customer)
* <li> fully qualified with database and table (e.g. salesapp.customer)
* </ul>
* <p>
* Both forms of input are accepted. All searches are done on
* a case-insensitive basis. In addition, the Progress 4GL schema
* supports optional abbreviation of tablenames portion. These
* abbreviations are valid as long as they represent a unique match. The
* smallest unique match to a table name is the smallest possible
* abbreviation. As such, no ambiguity is allowed in the name passed as
* an argument. There must be only one match that is ever found in the
* schema namespace.
* <p>
* In addition to the predefined table names from the schema, the user
* may define or create buffers, temp-tables and work-tables that all
* are interchangeable with table names. This represents a dynamic
* table namespace which must appear as a single unified space.
* <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
* Text string to match, in either of the 2 possible forms.
* @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 schema name or <code>null</code>
* if no match was found.
*/
public String lookupFullTableName(String name, boolean forceTemp, boolean forcePersistent)
{
String fqn = null;
try
{
if (schemaDict != null)
{
SchemaHelper<String> ftn = (SchemaDictionary dict) ->
{
return dict.getFullTableName(name, forceTemp, forcePersistent);
};
Predicate<String> notNull = (String val) -> { return (val == null); };
fqn = processHierarchy(ftn, notNull);
}
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return fqn;
}
/**
* Searches the schema dictionary for a table matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match, or one that failed due to an
* ambiguous table name or due to some other problem with the schema
* dictionary.
* <p>
* Schema table names can come in 2 forms:
* <ul>
* <li> unqualified, table name only (e.g. customer)
* <li> fully qualified with database and table (e.g. salesapp.customer)
* </ul>
* <p>
* Both forms of input are accepted. All searches are done on
* a case-insensitive basis. In addition, the Progress 4GL schema
* supports optional abbreviation of tablenames portion. These
* abbreviations are valid as long as they represent a unique match. The
* smallest unique match to a table name is the smallest possible
* abbreviation. As such, no ambiguity is allowed in the name passed as
* an argument. There must be only one match that is ever found in the
* schema namespace.
* <p>
* In addition to the predefined table names from the schema, the user
* may define or create buffers, temp-tables and work-tables that all
* are interchangeable with table names. This represents a dynamic
* table namespace which must appear as a single unified space.
*
* @param name
* Text string to match, in either of the 2 possible forms.
*
* @return The table's token type that was found or -1 if no match was
* found.
*/
public int lookupTable(String name)
{
NameNode table = lookupTableNode(name, false, false);
return (table == null) ? -1 : table.getType();
}
/**
* Searches the schema dictionary for a table matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match, or one that failed due to an
* ambiguous table name or due to some other problem with the schema
* dictionary.
* <p>
* Schema table names can come in 2 forms:
* <ul>
* <li> unqualified, table name only (e.g. customer)
* <li> fully qualified with database and table (e.g. salesapp.customer)
* </ul>
* <p>
* Both forms of input are accepted. All searches are done on
* a case-insensitive basis. In addition, the Progress 4GL schema
* supports optional abbreviation of tablenames portion. These
* abbreviations are valid as long as they represent a unique match. The
* smallest unique match to a table name is the smallest possible
* abbreviation. As such, no ambiguity is allowed in the name passed as
* an argument. There must be only one match that is ever found in the
* schema namespace.
* <p>
* In addition to the predefined table names from the schema, the user
* may define or create buffers, temp-tables and work-tables that all
* are interchangeable with table names. This represents a dynamic
* table namespace which must appear as a single unified space.
* <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
* Text string to match, in either of the 2 possible forms.
* @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 name node if found or {@code null} if no match was found.
*/
public NameNode lookupTableNode(String name, boolean forceTemp, boolean forcePersistent)
{
NameNode found = null;
try
{
if (schemaDict != null)
{
SchemaHelper<NameNode> ftt = (SchemaDictionary dict) ->
{
return dict.findTableNode(name, forceTemp, forcePersistent);
};
Predicate<NameNode> notNull = (NameNode val) -> { return val == null; };
found = processHierarchy(ftt, notNull);
}
}
catch (AmbiguousSchemaNameException exc)
{
// if not found, let the caller decide what to do (look again without forcing a specific
// db or fail).
if (!(forceTemp || forcePersistent))
{
LOG.log(Level.WARNING,"", exc);
}
return null;
}
catch (IllegalArgumentException | SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return found;
}
/**
* Searches the schema dictionary for a table matching the passed
* key and returns a boolean reporting if it is a valid table or not.
* <p>
* Schema table names can come in 2 forms:
* <ul>
* <li> unqualified, table name only (e.g. customer)
* <li> fully qualified with database and table (e.g. salesapp.customer)
* </ul>
* <p>
* Both forms are accepted as input. All searches are done on
* a case-insensitive basis. In addition, the Progress 4GL schema
* supports optional abbreviation of tablenames and fieldnames. These
* abbreviations are valid as long as they represent a unique match. The
* smallest unique match to a table or field name is the smallest
* possible abbreviation. As such, no ambiguity is allowed in the
* name passed as an argument. There must be only one match that is
* ever found in the schema namespace.
* <p>
* In addition to the predefined table names from the schema, the user
* may define or create buffers, temp-tables and work-tables that all
* are interchangeable with table names. This represents a dynamic
* table namespace which must appear as a single unified space.
* <p>
* <b>This method is safe to use inside a semantic predicate since it
* will not throw an exception if the name is not found or if there is
* ambiguity detected!</b>
*
* @param name
* Text string to match, in either of the supported 2 forms.
*
* @return true if the name is a valid field, false otherwise.
*/
public boolean isTable(String name)
{
boolean result = false;
if (schemaDict != null)
{
try
{
SchemaHelper<Boolean> isTbl =
(SchemaDictionary dict) -> { return dict.isTable(name); };
Predicate<Boolean> whileFalse = (Boolean val) -> { return (val == Boolean.FALSE); };
result = processHierarchy(isTbl, whileFalse);
}
catch (SchemaException exc)
{
// this should not happen
LOG.log(Level.SEVERE, "", exc);
}
}
return result;
}
/**
* Searches the schema dictionary for a field matching the passed
* name and then will return the token type of the backing construct
* (table, temp-table or work-table) to which this record belongs. Note
* that a token type of -1 is invalid in the Lexer and Parser, so this
* value is used to denote a search that found no match, or one that
* failed due to an ambiguous table name or due to some other problem with
* the schema dictionary.
* <p>
* Schema field names can come in 3 forms:
* <ul>
* <li> unqualified (e.g. address1)
* <li> partially qualified with a tablename (e.g. customer.address1)
* <li> fully qualified with database and table (e.g.
* salesapp.customer.address1)
* </ul>
* <p>
* All 3 forms are accepted as input. All searches are done on
* a case-insensitive basis. In addition, the Progress 4GL schema
* supports optional abbreviation of tablenames and fieldnames. These
* abbreviations are valid as long as they represent a unique match. The
* smallest unique match to a table or field name is the smallest
* possible abbreviation. As such, no ambiguity is allowed in the
* name passed as an argument. There must be only one match that is
* ever found in the schema namespace.
*
* @param name
* Text string to match, in any of the supported 3 forms.
*
* @return The backing record's token type that was found or -1 if no
* match was found.
*/
public int lookupFieldRecordType(String name)
{
int type = -1;
try
{
if (schemaDict != null)
{
SchemaHelper<Integer> frt =
(SchemaDictionary dict) -> { return dict.getFieldRecordType(name); };
Predicate<Integer> notNull =
(Integer val) -> { return (val == null || val.intValue() == -1) ; };
type = processHierarchy(frt, notNull);
}
}
catch (IllegalArgumentException | SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return type;
}
/**
* Searches the schema dictionary for a record matching the passed
* name and then will return the token type of the backing construct
* (table, temp-table or work-table) to which this record belongs. By
* definition, if one passes a name that references a table, temp-table
* or work-table directly, then this method returns the same token type
* value as can be obtained via {@link #lookupTable}. However, if the
* name references a buffer, then this method will return the type of
* the backing construct to which the buffer refers. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match, or one that failed due to an
* ambiguous table name or due to some other problem with schema
* dictionary.
* <p>
* Schema table names can come in 2 forms:
* <ul>
* <li> unqualified, table name only (e.g. customer)
* <li> fully qualified with database and table (e.g. salesapp.customer)
* </ul>
* <p>
* Both forms are accepted as input. All searches are done on
* a case-insensitive basis. In addition, the Progress 4GL schema
* supports optional abbreviation of tablenames and fieldnames. These
* abbreviations are valid as long as they represent a unique match. The
* smallest unique match to a table or field name is the smallest
* possible abbreviation. As such, no ambiguity is allowed in the
* name passed as an argument. There must be only one match that is
* ever found in the schema namespace.
* <p>
* In addition to the predefined table names from the schema, the user
* may define or create buffers, temp-tables and work-tables that all
* are interchangeable with table names. This represents a dynamic
* table namespace which must appear as a single unified space.
*
* @param name
* Text string to match, in either of the supported 2 forms.
*
* @return The backing record's token type that was found or -1 if no
* match was found.
*/
public int lookupTableRecordType(String name)
{
return lookupTableRecordType(name, false, false);
}
/**
* Searches the schema dictionary for a record matching the passed
* name and then will return the token type of the backing construct
* (table, temp-table or work-table) to which this record belongs. By
* definition, if one passes a name that references a table, temp-table
* or work-table directly, then this method returns the same token type
* value as can be obtained via {@link #lookupTable}. However, if the
* name references a buffer, then this method will return the type of
* the backing construct to which the buffer refers. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match, or one that failed due to an
* ambiguous table name or due to some other problem with schema
* dictionary.
* <p>
* Schema table names can come in 2 forms:
* <ul>
* <li> unqualified, table name only (e.g. customer)
* <li> fully qualified with database and table (e.g. salesapp.customer)
* </ul>
* <p>
* Both forms are accepted as input. All searches are done on
* a case-insensitive basis. In addition, the Progress 4GL schema
* supports optional abbreviation of tablenames and fieldnames. These
* abbreviations are valid as long as they represent a unique match. The
* smallest unique match to a table or field name is the smallest
* possible abbreviation. As such, no ambiguity is allowed in the
* name passed as an argument. There must be only one match that is
* ever found in the schema namespace.
* <p>
* In addition to the predefined table names from the schema, the user
* may define or create buffers, temp-tables and work-tables that all
* are interchangeable with table names. This represents a dynamic
* table namespace which must appear as a single unified space.
* <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
* Text string to match, in either of the supported 2 forms.
* @param forceTemp
* If <code>true</code>, force a temp-table lookup.
* @param forcePersistent
* If <code>true</code>, force a persistent table lookup.
*
* @return The backing record's token type that was found or -1 if no
* match was found.
*/
public int lookupTableRecordType(String name, boolean forceTemp, boolean forcePersistent)
{
int type = -1;
try
{
if (schemaDict != null)
{
SchemaHelper<Integer> trt = (SchemaDictionary dict) ->
{
return dict.getTableRecordType(name, forceTemp, forcePersistent);
};
Predicate<Integer> notNull =
(Integer val) -> { return (val == null || val.intValue() == -1) ; };
type = processHierarchy(trt, notNull);
}
}
catch (IllegalArgumentException | SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return type;
}
/**
* Searches the schema dictionary for a database matching the passed
* key. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used
* to denote a search that found no match, or one that failed due to an
* ambiguous database name or due to some other problem with the schema
* dictionary.
* <p>
* Schema database names are always unqualified (by '.' characters) and
* cannot be abbreviated. All searches are done on a case-insensitive
* basis. No ambiguity is allowed in the name passed as an argument.
* There must be only one match that is ever found in the schema namespace.
* <p>
* In addition to the predefined table names from the schema, the user
* may define or create database name aliases that are interchangeable
* with database names. This allows a procedure written to use a specific
* database name to use (at runtime) a database of a different name that
* has an identical or at least compatible schema. This represents a
* dynamic namespace which must appear as a single unified space.
* <p>
* <b>At this time, this method does not support the lookup of aliases
* since the dictionary does not provide such support.</b>
* <p>
* <b>This method is safe to use inside a semantic predicate since it
* will not throw an exception if the name is not found or if there is
* ambiguity detected!</b>
*
* @param name
* Text string to match.
*
* @return The database's token type that was found or -1 if no match
* was found.
*/
public int lookupDatabase(String name)
{
int type = -1;
// lookup in our local schema dictionary
if (schemaDict != null && schemaDict.isDatabase(name))
{
type = DATABASE;
}
return type;
}
/**
* Look up and return the field info instance which describes a database field in a schema,
* given the field's name and an optional table name-node.
*
* @param table
* Name node associated with the field reference. If not {@code null}, only the
* private namespace of the given {@code table} 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 name
* Field's name. May be qualified or not, but must be unambiguous in the current
* parsing context.
*
* @return Field's info instance, or <code>null</code> if <code>name</code> cannot be
* resolved to a field.
*/
public FieldInfo lookupFieldInfo(NameNode table, String name)
{
return lookupFieldInfo(table, name, false);
}
/**
* Look up and return the field info instance which describes a database field in a schema,
* given the field's name and an optional table name-node.
*
* @param table
* Name node asssociated with the field reference. If not {@code null}, only the
* private namespace of the given {@code table} 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 name
* Field's name. May be qualified or not, but must be unambiguous in the current
* parsing context.
* @param preferDefaultBuffer
* Allow unqualified fields to scope to the default buffer.
*
* @return Field's info instance, or <code>null</code> if <code>name</code> cannot be
* resolved to a field.
*/
public FieldInfo lookupFieldInfo(NameNode table, String name, boolean preferDefaultBuffer)
{
FieldInfo finfo = null;
try
{
SchemaHelper<FieldInfo> fget = (SchemaDictionary dict) ->
{
return dict.findFieldInfo(table, name, preferDefaultBuffer);
};
Predicate<FieldInfo> whileNull = (FieldInfo val) -> { return (val == null); };
finfo = processHierarchy(fget, whileNull);
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return finfo;
}
/**
* Look up and return the table info instance which describes a database table in a schema.
*
* @param name
* Text string to match, exactly as found in the original source code
* (abbreviated, qualified or unqualified).
* @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 info instance or <code>null</code> if no match was found.
*/
public TableInfo lookupTableInfo(String name, boolean forceTemp, boolean forcePersistent)
{
TableInfo result = null;
try
{
if (schemaDict != null)
{
SchemaHelper<TableInfo> info = (SchemaDictionary dict) ->
{
return dict.findTableInfo(name, forceTemp, forcePersistent);
};
Predicate<TableInfo> notNull = (TableInfo val) -> { return (val == null); };
result = processHierarchy(info, notNull);
}
}
catch (SchemaException exc)
{
LOG.log(Level.SEVERE, "", exc);
}
return result;
}
/**
* Look up and return the AST which represents a database field in a schema, given the field's
* name.
*
* @param name
* Field's name. May be qualified or not, but must be unambiguous in the current
* parsing context.
*
* @return Field's schema AST, or <code>null</code> if <code>name</code> cannot be resolved
* to a field.
*/
public Aast lookupFieldAst(String name)
{
Aast field = null;
try
{
SchemaHelper<Aast> fget = (SchemaDictionary dict) ->
{
Aast fld = null;
// getField() assumes that the node will be found, so we can
// only call it if we know the node exists in the current dictionary
if (dict.isField(name))
{
fld = dict.getField(name);
}
return fld;
};
Predicate<Aast> notNull = (Aast val) -> { return (val == null); };
field = processHierarchy(fget, notNull);
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return field;
}
/**
* Look up and return the AST which represents a database table in a schema, given the table's
* name.
*
* @param name
* Table's name. May be qualified or not, but must be unambiguous in the current
* parsing context.
*
* @return Table's schema AST, or <code>null</code> if <code>name</code> cannot be resolved
* to a table.
*/
public Aast lookupTableAst(String name)
{
Aast table = null;
try
{
SchemaHelper<Aast> fget = (SchemaDictionary dict) ->
{
Aast tbl = null;
// getTable() is not safe to call if the table doesn't exist in the
// current schema
if (dict.isTable(name))
{
tbl = dict.getTable(name);
}
return tbl;
};
Predicate<Aast> notNull = (Aast val) -> { return (val == null); };
table = processHierarchy(fget, notNull);
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return table;
}
/**
* 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 schema dictionary.
*
* @param table
* Target table AST.
*
* @return An iterator of fields for the specified table. May be empty, but will not be
* <code>null</code>.
*/
public Iterator<Aast> fields(String table)
{
Iterator<Aast> iter = null;
try
{
SchemaHelper<Iterator<Aast>> fget = (SchemaDictionary dict) ->
{
Aast tableAst = null;
// getTable() is not safe to call if the table doesn't exist in the
// current schema
if (dict.isTable(table))
{
tableAst = dict.getTable(table);
}
return (tableAst == null) ? null : dict.fields(tableAst);
};
Predicate<Iterator<Aast>> notNull = (Iterator<Aast> val) -> { return (val == null); };
iter = processHierarchy(fget, notNull);
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return (iter == null) ? EmptyIterator.get() : iter;
}
/**
* Allows the addition of a new table of any type (table, buffer,
* temp-table or work-table) to the schema namespace. Additions are
* made either to the global namespace (which is the same as the external
* procedure scope) or to the current internal procedure or function
* scope. This is user controlled by a parameter to this method call.
* <p>
* The database to which these are added is <code>null</code> and is
* picked up by context in the schema dictionary or the table is
* simply not associated with a specific database (true for temp-tables
* and work-tables at least).
* <p>
* Any of these token types are accepted as the 2nd parameter:
* <p>
* <ul>
* <li> TABLE (this should never really occur)
* <li> BUFFER
* <li> TEMP_TABLE
* <li> WORK_TABLE
* </ul>
* <p>
* In order to hide the schema dictionary interfaces from the caller,
* the <code>NameNode</code> object that can be used to reference
* the table (for the further addition of fields), is returned as a
* type <code>Object</code>. Field additions will require this instance
* reference to be passed as a parameter. See {@link #addField}.
*
* @param tablename
* The name of the table to add.
* @param type
* The integer token type representing the type of table.
* @param global
* Specifies if the table should be added to the global scope
* or to the current procedure/function scope.
* @param node
* The node that triggered the table creation.
* @param am
* Access modifiers if not null. Only will be non-null if this is a resource
* defined as a member of a class definition.
* @param st
* Static specifier if not null. Only will be non-null if this is a resource
* defined as a member of a class definition.
*
* @return The reference to the added table which is used in subsequent
* field adds. Returns <code>null</code> on error.
*/
public NameNode addTable(String tablename,
int type,
boolean global,
Aast node,
Aast am,
Aast st)
{
if (tablename == null || type < BEGIN_RECORDTYPES || type > END_RECORDTYPES)
{
return null;
}
ClassDefinition def = getCurrentClassDef();
// protected temp-tables and buffers are accessible from child classes
if ((!inMethod || isPreScan()) && def != null && (type == TEMP_TABLE || type == BUFFER))
{
int access = (am != null) ? am.getType() : -1;
boolean isStatic = (st != null && st.getType() == KW_STATIC);
def.addTable(node, tablename, type, access, isStatic, schemaDict, inMethod);
}
// make edits in our local schema dictionary
return schemaDict.addTableEntry(null, tablename, type, global, node);
}
/**
* Allows the addition of a new field of any Progress data type to a
* specific table in the schema namespace. Additions are
* always made to the global namespace, not to a specific scope.
* <p>
* Any of the valid field token types are accepted as the 3rd parameter
* (e.g. <code>ProgressParserTokenTypes.FIELD_INT</code>).
* <p>
* In order to hide the schema dictionary interfaces from the caller,
* the <code>NameNode</code> object that can be used to reference
* the table is required. This is an <code>Object</code> returned from a
* previous call to {@link #addTable}. This rule casts that to the
* proper type before calling the dictionary.
*
* @param table
* The table reference in which to add the field.
* @param field
* The name of the field to add.
* @param ftype
* The parser token type representing the field's data type.
* @param node
* Node in source file from which this new field creation was
* triggered.
*
* @return The <code>Object</code> representing the added field or
* <code>null</code> on failure.
*/
public Object addField(Object table, String field, int ftype, Aast node)
{
if (table == null || field == null || ftype < BEGIN_FIELDTYPES || ftype > END_FIELDTYPES)
{
return null;
}
// make edits in our local schema dictionary
return (Object) schemaDict.addFieldEntry((NameNode)table,
node,
field,
ftype,
true);
}
/**
* Allows the addition of a new field of any Progress data type to a
* specific table in the schema namespace based on reference to an already
* existing variable or field. Additions are always made to the global
* namespace, not to a specific scope.
* <p>
* Depending on the token type stored in the 3rd parameter, 2 possible
* outcomes can occur:
* <p>
* <ol>
* <li> If the AST represents a variable type, then the field's options
* will be set to any non-default values stored in the variable
* definition.
* <li> If the AST is a field, then the schema dictionary will create
* the field with its options set the same as the referenced
* field.
* </ol>
* <p>
* In order to hide the schema dictionary interfaces from the caller,
* the <code>NameNode</code> object that can be used to reference
* the table is required. This is an <code>Object</code> returned from a
* previous call to {@link #addTable}. This rule casts that to the
* proper type before calling the dictionary.
*
* @param table
* The table reference in which to add the field.
* @param field
* The name of the field to add.
* @param ast
* The AST node being used as a reference variable or field.
*
* @return The <code>Aast</code> instance representing the added field or
* <code>null</code> on failure.
*/
public Aast addFieldLike(Object table, String field, Aast ast)
{
if (table == null || field == null || ast == null)
return null;
int ftype = ast.getType();
int extent = getExtentSubscript(ast);
String oldname = ast.getText();
boolean isFld = ftype > BEGIN_FIELDTYPES && ftype < END_FIELDTYPES;
if (isFld)
{
NameNode node = null;
// this field should be LIKE another field, call the schema
// dictionary to add the field.
try
{
// find the source field's name node, even if it is in a different schema dict
NameNode source = null;
SchemaHelper<NameNode> ffn =
(SchemaDictionary dict) -> { return dict.findFieldNode(oldname); };
Predicate<NameNode> notNull = (NameNode val) -> { return (val == null); };
source = processHierarchy(ffn, notNull);
if (source == null)
{
throw new SchemaException("Field '" + oldname + "' not found");
}
// add to our local schema dictionary, using the (possibly remote) source node
node = schemaDict.addFieldEntry((NameNode) table,
ast,
field,
source,
extent);
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return node.getAst();
}
// OK, we are add a field like a variable, so we add the field and then
// copy our variable options into it
int type = -1;
// the VAR_ type must be converted to the proper FIELD_ type
switch (ftype)
{
case VAR_CHAR:
type = FIELD_CHAR;
break;
case VAR_CLASS:
type = FIELD_CLASS;
break;
case VAR_COM_HANDLE:
type = FIELD_COM_HANDLE;
break;
case VAR_DATE:
type = FIELD_DATE;
break;
case VAR_DATETIME:
type = FIELD_DATETIME;
break;
case VAR_DATETIME_TZ:
type = FIELD_DATETIME_TZ;
break;
case VAR_DEC:
type = FIELD_DEC;
break;
case VAR_HANDLE:
type = FIELD_HANDLE;
break;
case VAR_INT:
type = FIELD_INT;
break;
case VAR_INT64:
type = FIELD_INT64;
break;
case VAR_LOGICAL:
type = FIELD_LOGICAL;
break;
case VAR_RAW:
type = FIELD_RAW;
break;
case VAR_RECID:
type = FIELD_RECID;
break;
case VAR_ROWID:
type = FIELD_ROWID;
break;
default:
// this MUST be at the end of the list of variables, it will
// match LONGCHAR and MEMPTR which cannot be created as a field
if (ftype > BEGIN_VARTYPES && ftype < BEGIN_VARTYPES)
{
String err = String.format("Unexpected variable type %s!",
ast.getDescriptiveTokenText());
throw new RuntimeException(err);
}
break;
}
if (type == -1)
return null;
Variable var = (Variable) lookupWrapped(varDict, oldname);
NameNode node = null;
try
{
// make edits in our local schema dictionary
node = schemaDict.addFieldEntry((NameNode) table,
ast,
field,
type,
var.getOptions(extent));
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return node.getAst();
}
/**
* Inspect the field definition and write any options into the field
* object.
*
* @param name
* The name of the field which must have its options set.
* @param ast
* The tree to inspect which defines a Progress field.
*/
public void setFieldOptions(String name, Aast ast)
{
try
{
// make edits in our local schema dictionary
schemaDict.setFieldProperties(name, ast);
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
}
/**
* Allows the addition of a list of fields (as defined in an existing
* table) to a specific table in the schema namespace. Additions are
* always made to the global namespace, not to a specific scope.
* <p>
* In order to hide the schema dictionary interfaces from the caller,
* the <code>NameNode</code> object that can be used to reference
* the table is required. This is an <code>Object</code> returned from a
* previous call to {@link #addTable}. This rule casts that to the
* proper type before calling the dictionary.
*
* @param table
* The table reference in which to add fields.
* @param tablename
* The name of the table whose fields should be added.
*
* @return The list of ASTs representing the copied fields.
*/
public Aast[] addFieldsFrom(Object table, String tablename)
{
return addFieldsFrom(table, tablename, false, false, true);
}
/**
* Allows the addition of a list of fields (as defined in an existing
* table) to a specific table in the schema namespace. Additions are
* always made to the global namespace, not to a specific scope.
* <p>
* In order to hide the schema dictionary interfaces from the caller,
* the <code>NameNode</code> object that can be used to reference
* the table is required. This is an <code>Object</code> returned from a
* previous call to {@link #addTable}. This rule casts that to the
* proper type before calling the dictionary.
*
* @param table
* The table reference in which to add fields.
* @param tablename
* The name of the table whose fields should be added.
* @param skipGlobal
* if (@code true}, do not look in the global schema scope to find
* {@code tablename}.
* @param sequential
* <code>true</code> to add fields based on the <code>ORDER</code> schema property
* which is the equivalent of <code>LIKE-SEQUENTIAL</code>. <code>false</code> to
* add fields based on the <code>POSITION</code> schema property, which is the same
* as <code>LIKE</code> (this is the original way that the 4GL did it).
* @param fail
* Flag indicating if an exception should be thrown if the source table is not
* found.
*
* @return The list of ASTs representing the copied fields.
*/
public Aast[] addFieldsFrom(Object table,
String tablename,
boolean skipGlobal,
boolean sequential,
boolean fail)
{
List<Aast> fields = null;
try
{
NameNode toTable = (NameNode) table;
// find the source field's name node, even if it is in a different schema dict
SchemaHelper<NameNode> ftfn = (SchemaDictionary dict) ->
{
NameNode nn = null;
// findTableFromNode() is not safe to call if the table doesn't exist in the
// current schema
if (dict.isTable(tablename))
{
nn = dict.findTableFromNode(tablename, toTable, skipGlobal);
}
return nn;
};
Predicate<NameNode> notNull = (NameNode val) -> { return (val == null); };
NameNode fromTable = processHierarchy(ftfn, notNull);
if (fromTable == null)
{
if (fail)
{
throw new SchemaException("'From' table not found: " + tablename);
}
else
{
return null;
}
}
// make edits in our local schema dictionary
// TODO: add support for proper ordering based on the sequential flag, it is likely that
// the existing approach is wrong as well (that it doesn't follow the POSITION
// attr)
schemaDict.addFieldEntries(tablename, fromTable, toTable, skipGlobal);
fields = new ArrayList<Aast>();
Aast child = (Aast) toTable.getAst().getFirstChild();
while (child != null)
{
if (child.getType() > BEGIN_FIELDTYPES && child.getType() < END_FIELDTYPES)
{
fields.add(child);
}
child = (Aast) child.getNextSibling();
}
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
return fields == null ? null : fields.toArray(new Aast[0]);
}
/**
* Adds an index from the original source table into the temp-table that
* is passed in. Additions are always made to the global namespace, not
* to a specific scope.
* <p>
* In order to hide the schema dictionary interfaces from the caller,
* the <code>NameNode</code> object that can be used to reference
* the table is required. This is an <code>Object</code> returned from a
* previous call to {@link #addTable}. This rule casts that to the
* proper type before calling the dictionary.
*
* @param table
* The table reference in which to add fields.
* @param node
* Node in source file from which this new field creation was
* triggered.
* @param name
* The name of the index to add.
* @param primary
* Whether or not the <code>PRIMARY</code> keyword was present.
*/
public void addIndexFrom(Object table,
Aast node,
String name,
boolean primary)
{
if (table == null || name == null)
{
// nothing to do
return;
}
// add "use index" processing to schema dictionary
try
{
// make edits in our local schema dictionary uses (possibly remote) state that has
// been previously setup
schemaDict.copyIndex((NameNode) table, node, name, primary);
}
catch (SchemaException exc)
{
// don't abend here, the 4GL allows garbage here, just warn the user
System.out.printf("WARNING: USE-INDEX processing failed in DEFINE TEMP-TABLE LIKE, " +
"this is not fatal because the 4GL allows garbage to be specified" +
" (%s).\n",
exc.getMessage());
}
}
/**
* Adds an index as defined in the source code into the temp-table that
* is passed in. Additions are always made to the global namespace, not
* to a specific scope.
* <p>
* In order to hide the schema dictionary interfaces from the caller,
* the <code>NameNode</code> object that can be used to reference
* the table is required. This is an <code>Object</code> returned from a
* previous call to {@link #addTable}. This rule casts that to the
* proper type before calling the dictionary.
*
* @param table
* The table reference in which to add fields.
* @param name
* The name of the index to add.
* @param ast
* The AST storing the nodes that define the index.
*/
public void addIndex(Object table, String name, Aast ast)
{
if (table == null || name == null || ast == null)
{
// nothing to do
return;
}
try
{
// make edits in our local schema dictionary
// add a new index for the given table by the given name and
// set the options based on our AST
schemaDict.addIndex((NameNode) table, name, ast);
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
}
/**
* Sets specific conditions on a temp-table definition based on
* encountering certain tokens in the source code. The following tokens
* are known: <code>NO-UNDO, VALIDATE, RCODE-INFORMATION</code>.
* <p>
* In order to hide the schema dictionary interfaces from the caller,
* the <code>NameNode</code> object that can be used to reference
* the table is required. This is an <code>Object</code> returned from a
* previous call to {@link #addTable}. This rule casts that to the
* proper type before calling the dictionary.
*
* @param table
* The table reference which is completed.
* @param token
* The type of the option that was encountered in the source
* code.
* @param node
* The node that triggered the table creation.
*/
public void setTableOption(Object table, int token, Aast node)
{
// access our local schema dictionary
Aast tableAst = schemaDict.getTable((NameNode) table);
try
{
Aast prop = new ProgressAst();
prop.setType(token);
prop.setLine(node.getLine());
prop.setColumn(node.getColumn());
switch (token)
{
case KW_NO_UNDO:
{
// set NO-UNDO option for this table
prop.setText("no-undo");
break;
}
case KW_VALIDATE:
// set VALIDATE option for this table
prop.setText("validate");
break;
case KW_RCOD_INF:
{
// set RCODE-INFORMATION option for this table
prop.setText("rcode-information");
break;
}
case KW_LIKE_SEQ:
{
// set LIKE-SEQUENTIAL option for this table
prop.setText("like-sequential");
break;
}
}
// make edits in our local schema dictionary
schemaDict.setTableProperty(tableAst, prop);
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
}
/**
* Notifies the schema dictionary that the end of a temp-table definition
* has been reached. This allows certain default processing to be executed
* as needed.
* <p>
* In order to hide the schema dictionary interfaces from the caller,
* the <code>NameNode</code> object that can be used to reference
* the table is required. This is an <code>Object</code> returned from a
* previous call to {@link #addTable}. This rule casts that to the
* proper type before calling the dictionary.
*
* @param table
* The table reference which is completed.
* @param ast
* The created table AST.
*/
public void finalizeTableDefine(Object table, Aast ast)
{
try
{
// notify the local schema dictionary of the "end of temp-table definition"
schemaDict.finalizeTableDefine((NameNode) table, ast);
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
}
/**
* Adds a record scope to the schema namespace to the top of the stack
* of scopes. Any tables that are promoted (see {@link #promoteTable})
* within this scope are given precedence in making their field names
* unambiguous when compared to fields of the same name in tables that
* are in scopes that are lower on the stack.
*
* @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 addSchemaScope(boolean mark)
{
if (schemaDict != null)
{
schemaDict.addScope(mark);
}
}
/**
* Deletes the current record scope in the schema namespace (the top of
* the stack of scopes). Any tables that were promoted (see
* {@link #promoteTable}) into this scope are naturally popped off the
* stack by this action if <code>propagate</code> is <code>false</code>.
* Otherwise the namespace updates made in this scope will be copied to
* the namespaces of the enclosing scope.
*
* @param propagate
* <code>true</code> to force namespace propagation to the
* enclosing scope.
*/
public void deleteSchemaScope(boolean propagate)
{
try
{
if (schemaDict != null)
{
schemaDict.removeScope(propagate);
}
}
catch (SchemaException exp)
{
// ignore it, we don't care!
}
}
/**
* Globally enable or disable schema table name promotion.
*
* @param allow
* <code>true</code> to enable, <code>false</code> to disable.
*/
public void allowPromotion(boolean allow)
{
this.allow = allow;
}
/**
* Promotes a table (and all its fields) into the current top scope of
* the schema dictionary's stack of scopes. Any tables that are promoted
* within this scope are given precedence in making their field names
* unambiguous when compared to fields of the same name in tables that
* are in scopes that are lower on the stack.
* <p>
* Scopes are created using {@link #addSchemaScope} and are deleted using
* {@link #deleteSchemaScope}.
*
* @param table
* The name of the table (optionally qualified) to promote.
* @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.
*/
public void promoteTableName(String table, boolean force, boolean noProp)
{
promoteTableName(table, force, noProp, true);
}
/**
* Promotes a table (and all its fields) into the current top scope of
* the schema dictionary's stack of scopes. Any tables that are promoted
* within this scope are given precedence in making their field names
* unambiguous when compared to fields of the same name in tables that
* are in scopes that are lower on the stack.
* <p>
* Scopes are created using {@link #addSchemaScope} and are deleted using
* {@link #deleteSchemaScope}.
*
* @param table
* The name of the table (optionally qualified) to promote.
* @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.
*/
public void promoteTableName(String table, boolean force, boolean noProp, boolean self)
{
try
{
if (allow)
{
// we only promote if the name is in the local schema, there is no
// scoping or need to scope the schema dictionaries in the class
// hierarchy, because the temp-tables are already at the global
// scope in those dictionaries
if (schemaDict.isTable(table))
{
schemaDict.promoteTable(table, force, noProp, self);
}
}
}
catch (SchemaException exc)
{
LOG.log(Level.WARNING,"", exc);
}
}
/**
* Promote all tables in the specified schema, to the current scope.
*
* @param schemaname
* The schema.
*
* @see SchemaDictionary#promoteTable(String)
*/
public void promoteAllTables(String schemaname)
{
String schema = schemaname.substring(0, schemaname.indexOf('.'));
Iterator<Aast> iter = schemaDict.tables(schema);
while (iter.hasNext())
{
Aast table = iter.next();
try
{
schemaDict.promoteTable(schema + '.' + table.getText());
}
catch (SchemaException e)
{
LOG.log(Level.WARNING,"", e);
}
}
}
/**
* Promotes a table (and all its fields) into the current top scope of
* the schema dictionary's stack of scopes. Any tables that are promoted
* within this scope are given precedence in making their field names
* unambiguous when compared to fields of the same name in tables that
* are in scopes that are lower on the stack.
* <p>
* Scopes are created using {@link #addSchemaScope} and are deleted using
* {@link #deleteSchemaScope}.
*
* @param table
* The <code>Object</code> reference representing a table
* that had been previously added to the schema dictionary.
* @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.
*/
public void promoteTable(Object table, boolean force, boolean noProp, boolean self)
{
promoteTableName(((NameNode) table).getName(), force, noProp, self);
}
/**
* 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
*/
public void processLikeField(Aast field, boolean hasValidate, String sourceSchemaName)
{
try
{
if (schemaDict != null)
{
// make edits in our local schema dictionary
schemaDict.processLikeField(field, hasValidate, sourceSchemaName);
}
}
catch (AmbiguousSchemaNameException exc)
{
LOG.log(Level.WARNING,"", exc);
}
}
/**
* Prints a report to <code>stderr</code> with the listing of each
* dictionary. One record per dictionary entry will be printed and
* there will be a section for each namespace type.
*/
public void dump()
{
StringOutputStream outputStream = new StringOutputStream();
PrintStream ps = new PrintStream(outputStream);
ps.println("\n-------------Keyword Listing-------------");
kwDict.dump(ps);
ps.println("\n-------------Variable Listing-------------");
varDict.dump(ps);
ps.println("\n-------------DATASET Listing-------------");
dsetDict.dump(ps);
ps.println("\n------------DS-SOURCE Listing-------------");
dsrcDict.dump(ps);
ps.println("\n-------------Function Listing-------------");
funcDict.dump(ps);
ps.println("\n-------------Procedure Listing-------------");
procDict.dump(ps);
ps.println("\n---------------Label Listing---------------");
labelDict.dump(ps);
ps.println("\n--------------Widget Listing--------------");
widgetDict.dump(ps);
ps.println("\n---------------Frame Listing---------------");
frameDict.dump(ps);
ps.println("\n-----------Field Widget Listing-----------");
fieldWidgetDict.dump(ps);
ps.println("\n----------- Frame USE-DICT-EXPS Listing-----------");
useDictExps.dump(ps);
ps.println("\n---------------Menu Listing---------------");
menuDict.dump(ps);
ps.println("\n-------------Menu Item Listing-------------");
menuItemDict.dump(ps);
ps.println("\n--------------Stream Listing--------------");
streamDict.dump(ps);
LOG.log(Level.INFO, outputStream.getData());
}
/**
* Prints a report to <code>schema.dmp</code> with the listing of all
* current contents of the Schema Dictionary.
*/
public void dumpSchema()
throws IOException,
FileNotFoundException
{
FileOutputStream fout = null;
PrintStream ps = null;
try
{
if (schemaDict != null)
{
File file = new File("schema.dmp");
fout = new FileOutputStream(file);
ps = new PrintStream(fout);
schemaDict.dump(ps);
System.out.println(file.getAbsolutePath() + " dump file created");
}
}
finally
{
if (ps != null)
{
ps.close();
}
else if (fout != null)
{
fout.close();
}
}
}
/**
* Prints a report of the given class' .NET dependencies to the file
* <code>dotnet_dependencies.txt</code>.
*
* @param cls
* The class for which to dump the dependencies.
*/
public void dumpDotNetDependencies(ClassDefinition cls)
{
FileOutputStream fout = null;
PrintStream ps = null;
try
{
if (cls != null && cls.isIndirectDotNetReference())
{
File file = new File("dotnet_dependencies.txt");
// open in append mode
fout = new FileOutputStream(file, true);
// auto-flush
ps = new PrintStream(fout, true);
String type = cls.isClass() ? "CLASS" : (cls.isInterface() ? "INTERFACE" : "ENUM");
ps.printf("%s %s\n---\n", type, cls.getName());
if (cls.isDerivedFromDotNet())
{
for (String cname : cls.getDerivedDotNet())
{
ps.printf("DERIVED_FROM %s\n", cname);
}
}
if (cls.hasDirectDotNet())
{
for (String cname : cls.getDirectDotNet())
{
ps.printf("DIRECT_REF %s\n", cname);
}
}
if (cls.hasIndirectDotNet())
{
for (String cname : cls.getIndirectDotNet())
{
ps.printf("INDIRECT_REF %s\n", cname);
}
}
ps.printf("\n");
}
}
catch (IOException e)
{
LOG.log(Level.SEVERE, "Failure in .NET dependencies output generation.", e);
}
catch (Throwable t)
{
LOG.log(Level.SEVERE, "Non-I/O Failure in .NET dependencies output generation.", t);
}
finally
{
try
{
if (ps != null)
{
ps.close();
}
else if (fout != null)
{
fout.close();
}
}
catch (IOException ioe)
{
// ignore
}
}
}
/**
* Returns the token type from the passed wrapped object or -1 for an
* is invalid {@link TokenDataWrapper} object.
*
* @param wrapped
* The object from which to obtain the token type.
*
* @return The token type that was found or -1 for an invalid wrapper
* object.
*/
public int lookupWrappedType(TokenDataWrapper wrapped)
{
return wrapped != null ? wrapped.getTokenType() : -1;
}
/**
* Set the {@link SchemaDictionary#markPreferred} flag, so all info for the tables which are
* weak references will be collected in the preferred nodes for the current scope.
*
* @param preferred
* The new value of the flag.
*
* @return The old value of the {@link SchemaDictionary#markPreferred} field.
*/
public boolean markPreferred(boolean preferred)
{
boolean old = false;
if (schemaDict != null)
{
// make edits in our local schema dictionary
old = schemaDict.markPreferred(preferred);
}
return old;
}
/**
* Set the {@link SchemaDictionary#markStrongScope} flag, so all info for the tables which are
* strong references will be stored for special disambiguation in the current scope.
*
* @param strong
* The new value of the flag.
*/
public void markStrongScope(boolean strong)
{
if (schemaDict != null)
{
// make edits in our local schema dictionary
schemaDict.markStrongScope(strong);
}
}
/**
* Mark the state that lookups are done in a chain.
*
* @param chain
* Flag indicating if we are in a chain reference.
*/
public void setChainedReference(boolean chain)
{
this.chainedReference = chain;
}
/**
* Get the state of the {@link #chainedReference} flag.
*
* @return See above.
*/
public boolean isChainedReference()
{
return chainedReference;
}
/**
* Check if we are running in a FWD server runtime.
*
* @return The {@link Configuration#isRuntimeConfig()} value.
*/
protected boolean isRuntimeConfig()
{
return Configuration.isRuntimeConfig();
}
/**
* Load the given class and create a class definition.
*
* @param name
* The fully qualified Java class name to load.
*
* @return The class definition wrapping the Java class.
*/
private ClassDefinition loadJavaClass(String name)
{
ClassDefinition cls = null;
try
{
cls = new JavaClassDefinition(Class.forName(name));
classDict.addSymbol(false, name, cls);
}
catch (Throwable thr)
{
// most likely this was a ClassNotFoundException but there also could have been a
// LinkageError or ExceptionInInitializerError (both are abnormal ends)
// ignore, let things return null below
// Only use this for debugging. This method can be called in a "try" mode where a failure
// must not be fatal and must not be verbose.
LOG.log(Level.FINEST, "Could not resolve java class: " + name, thr);
}
return cls;
}
/**
* Check if the given name is a valid Java class.
*
* @param name
* The fully qualified Java class name to check.
* @param result
* Object in which to store the search results (the class
* name, the built-in flag and the .NET flag) when the search
* is successful. If nothing is found, this object is unchanged.
*
* @return {@code true} if the class is a valid Java class.
*/
private boolean isJavaClass(String name, SearchResult result)
{
Class<?> cls = null;
try
{
cls = Class.forName(name);
// if we are here, the Java class exists
result.clsname = name;
result.builtin = false;
result.dotnet = false;
result.java = true;
result.type = UsingType.JAVA;
}
catch (LinkageError exc)
{
// log linkage errors
LOG.log(Level.SEVERE, "Could not load Java class " + name, exc);
}
catch (Throwable thr)
{
// most likely this was a ClassNotFoundException but there also could have been a
// LinkageError or ExceptionInInitializerError (both are abnormal ends)
// ignore, let things detect null below
}
return (cls != null);
}
/**
* Ensure the entire class hierarcy (from super-classes and implementing interfaces) is parsed,
* so that the provisional members are resolved.
*
* @param clsName
* The qualified legacy name.
* @param wa
* The work area.
*
* @throws NullPointerException
* If a parent class can not be found.
*/
private void parseHierarchy(String clsName, WorkArea wa)
throws NullPointerException
{
ClassDefinition cdef = getClassDefinition(clsName);
if (cdef == null)
{
throw new NullPointerException("Can not find class " + clsName);
}
if (cdef.isProcessed())
{
return;
}
if (cdef.getInterfaces() != null)
{
for (String iface : cdef.getInterfaces())
{
parseHierarchy(iface, wa);
}
}
ClassDefinition[] parents = cdef.getParents();
if (parents != null)
{
for (ClassDefinition parent : parents)
{
parseHierarchy(parent.getName(), wa);
}
}
cdef.setProcessed(true);
String file = cdef.getFilename();
if (!cdef.isBuiltIn() && new File(file).exists())
{
generator.processFile(file, null, -1);
// at this point we have been fully parsed and all parents have been fully parsed, so
// it is safe to detect whether or not one of our ancestors is a .NET class
cdef.derivedFromDotNet();
// write the dependencies into a log file
dumpDotNetDependencies(cdef);
}
}
/**
* Searches a definition in the local (or upper) scopes.
*
* @param name
* The member to search for.
*
* @return The found variable member or <code>null</code> otherwise.
*/
private Variable lookupLocalDef(String name)
{
return (Variable) lookupWrapped(name, accumDict, varDict, widgetDict, menuDict, menuItemDict);
}
/**
* Searches the named class' hierarchy for the named data member.
*
* @param cname
* Class name to search within or <code>null</code> if this is
* a member within the local class definition.
* @param name
* Member name to lookup.
* @param isStatic
* If <code>true</code>, only static methods will be looked up.
* Otherwise any method can be returned.
*
* @return The token type of the member or -1 if no match was found.
*/
private int lookupDataMemberWorker(String cname, String name, boolean isStatic)
{
ClassDefinition cls = (cname == null) ? getCurrentClassDef() : lookupClass(cname);
if (cls == null)
{
return -1;
}
int access = calcAccessMode(cname);
Variable clsVar = cls.lookupVariableWrapper(name, access, isStatic, chainedReference);
if (clsVar == null)
{
return -1;
}
// ensure we do not have a local var scoped to the method, with the same name
if (inMethod && !classLookup)
{
Variable localVisibleVar = (Variable) varDict.lookupSymbol(name);
if (localVisibleVar != null && localVisibleVar != clsVar)
{
return -1;
}
}
return clsVar.getTokenType();
}
/**
* Determine the parent class based on the optional inherits clause and then load or
* return the existing class definition(s).
*
* @param name
* Text defining the fully qualified (including package) child class name.
* @param inherits
* Node defining one or more fully qualified superclass names or <code>null</code>
* for none.
* @param dotNet
* Flag indicating this is a .NET class name.
*
* @return The parent class definitions or <code>null</code> if Progress.Lang.Object is
* the child class. Only interfaces can have multiple inheritance.
*/
private ClassDefinition[] calculateParents(String name, Aast inherits, boolean dotNet)
{
WorkArea wa = locate();
ClassDefinition[] parents = null;
ClassDefinition parent = null;
if (inherits != null)
{
int num = inherits.getNumImmediateChildren();
parents = new ClassDefinition[num];
for (int i = 0; i < num; i++)
{
Aast child = inherits.getChildAt(i);
String pname = (String) child.getAnnotation("qualified");
if (pname == null)
{
String err = String.format("Missing annotation (qualified) in inherits node\n%s", child.dumpTree(true));
throw new RuntimeException(err);
}
parent = lookupClass(pname);
if (parent == null)
{
// will cause recursion if successful
loadClass(pname);
parent = lookupClass(pname);
}
if (parent == null)
{
if (wa.preScanRefs.contains(pname))
{
String err = String.format("Parent class already in pre-scan - postponed: %s!", pname);
throw new ParentUnavailableException(err);
}
String err = String.format("Missing parent class definition for %s!", pname);
throw new RuntimeException(err);
}
parents[i] = parent;
}
}
else if (dotNet)
{
if (!ROOT_DOTNET_OBJ_NAME.equals(name))
{
// default parent for all classes
loadClass(ROOT_DOTNET_OBJ_NAME);
parent = lookupClass(ROOT_DOTNET_OBJ_NAME);
if (parent != null)
parents = new ClassDefinition[] { parent };
}
}
else if (!ROOT_OBJ_NAME.equals(name))
{
// default parent for all classes
loadClass(ROOT_OBJ_NAME);
parent = lookupClass(ROOT_OBJ_NAME);
if (parent != null)
parents = new ClassDefinition[] { parent };
}
return parents;
}
/**
* Initialize the maps of all possible class and interface definitions that
* exist in the project. This includes all <code>.cls</code> files which can
* be found via the <code>PROPATH</code> as well as all Progress built-ins
* and all .NET classes/interfaces that can be accessed.
* <p>
* These files will be listed from the directories represented by the
* {@link #propath}, {@link #OO4GL_PATH}, {@link #DOTNET_PATH} and the
* {@link #ASSEMBLY_PATH}.
* <p>
* If file-system case-sensitivity is <code>false</code> then all map keys
* will be lowercased.
*
* @param propathCls
* The map to store the classes.
* @param propath
* The propath to scan for initialization. The propath can change
* with each instantiation of this class. If it changes then
* the propath class mappings must be reinitialized. This may be
* <code>null</code> in which case the map will be empty.
* @param caseSens
* <code>true</code> if the legacy system had a case-sensitive
* file-system.
*/
private static synchronized ScopedSymbolDictionary initPossibleClasses(Map<String, List<String>> propathCls,
String[] propath,
boolean caseSens)
{
String propathKey = "";
WorkArea wa = locate();
if (propath != null)
{
boolean curDir = false;
for (int i = 0; i < propath.length; i++)
{
propathKey = propathKey + (propath[i] == null ? "<null>" : propath[i]) + "$$$";
if (propath[i] != null)
{
String srch = propath[i];
if (srch.length() == 0 || srch.equals("."))
{
// only search the current directory once
if (curDir)
continue;
srch = ".";
curDir = true;
}
createClassMappings(propathCls,
getClassFileList(srch, false),
srch,
false);
}
}
}
// all classes are loaded globally - the current appraoch of keeping (propath, classdict)
// is incorrect, we need to keep a (classQName, (file, ClassDefinition)) mapping, and
// resolve the correct definition based on the current PROPATH
propathKey = "";
// TODO: solve this
ScopedSymbolDictionary classDict = wa.propathClassDicts.get(propathKey);
if (classDict == null)
{
classDict = new ScopedSymbolDictionary<>(null, false);
wa.propathClassDicts.put(propathKey, classDict);
}
// add the built-ins, .net and custom fakeout classes/interfaces; this
// only happens the first time this method is called
// TODO: this assumes the caseSens value is the same project-wide AND
// that the correct value is passed in the first time the
// class is instantiated (which can be by the SchemaLoader)
if (wa.oo4glCls == null)
{
// 4GL classes are matched case-insensitively (even on case-sensitive file systems)
wa.oo4glCls = new HashMap<String, List<String>>();
createClassMappings(wa.oo4glCls,
getClassFileList(oo4glPath, false),
oo4glPath,
false);
}
if (wa.dotnetCls == null)
{
wa.dotnetCls = new HashMap<String, List<String>>();
createClassMappings(wa.dotnetCls,
getClassFileList(dotnetPath, caseSens),
dotnetPath,
caseSens);
}
if (wa.assemblyCls == null)
{
wa.assemblyCls = new HashMap<String, List<String>>();
createClassMappings(wa.assemblyCls,
getClassFileList(ASSEMBLY_PATH, caseSens),
ASSEMBLY_PATH,
caseSens);
}
return classDict;
}
/**
* List the 4GL class/interface files in a given path.
*
* @param path
* The directory to search.
*
* @return The list of files that exist. If there are no files, the
* array will have a 0 length.
*/
private static ArrayList<File> getClassFiles(String path)
{
ArrayList<File> results = new ArrayList<File>();
scan(new File(path), results);
return results;
}
/**
* Core implementation of the listing algorithm which uses the
* <code>File</code> class to make a complete list of the directory
* contents, then this list is used to build a filtered list of just
* those files that match the user-defined regular expression. Note
* that if recursion was previously requested (on construction) by the
* caller, this method will recursively call itself for every directory
* it encounters in the list. Only files will be returned in the results
* list and all results are added to the results list that is passed as
* a parameter.
*
* @param dir
* The directory in which to list all files.
* @param results
* The list to which all matching files must be appended.
*/
private static void scan(File dir, ArrayList<File> results)
{
// we don't use a FilenameFilter here because this would filter out
// all directories that don't match the regular expression, but this
// would not be correct, we need to see all directories and we will
// manually filter the filenames down ourselves
File[] rawResults = dir.listFiles();
// separate file and directory results
for (int i = 0; i < rawResults.length; i++)
{
if (rawResults[i].isDirectory())
{
// recursively call ourselves to process this subdirectory
scan(rawResults[i], results);
}
else
{
// only accept matches that meet the user's criteria (regular
// expression)
if (rawResults[i].getName().endsWith(CLASS_EXT))
{
// add the file to the results list
results.add(rawResults[i]);
}
}
}
}
/**
* Attempt to match the {@link #sortFields} as a prefix for the {@link #indexes} in the
* {@link #indexTable}.
*
* @param exact
* Flag indicating if prefix index match is allowed (when <code>false</code>);
* otherwise, the index must be matched fully.
*
* @return <code>true</code> if a match was possible.
*
* @throws SchemaException
* If we are not able to match partially an index prefix, and there are
* inconsistencies between unqualified field names (they can be resolved to multiple
* or another table).
*/
private boolean indexPrefixMatch(boolean exact)
throws SchemaException
{
int foundIndex = -1;
// walk each index and check if it matches as a prefix for the collected fields
Iterator<Aast> iter = indexes.keySet().iterator();
while (iter.hasNext())
{
Aast next = iter.next();
List<Aast> fields = indexes.get(next);
if (exact && fields.size() > sortFields.size())
{
// can't match as prefix
continue;
}
boolean match = true;
int idx = 0;
for (Aast idxField : fields)
{
Aast sortField = sortFields.get(idx);
boolean idxAsc = !idxField.downPath(KW_DESCEND);
boolean sortAsc = sortField.getNextSibling() == null ||
sortField.getNextSibling().getType() != KW_DESCEND;
if (idxAsc != sortAsc)
{
match = false;
break;
}
if (sortField.getType() == EXPRESSION)
{
sortField = (Aast) sortField.getFirstChild();
}
String sortName = (String) sortField.getAnnotation("name");
if (sortName.indexOf(".") == -1)
{
sortName = String.format("%s.%s", indexTable, sortName);
}
Aast fieldAst = lookupFieldAst(sortName);
if (fieldAst == null || !fieldAst.getText().equalsIgnoreCase(idxField.getText()))
{
match = false;
break;
}
idx = idx + 1;
if (idx >= sortFields.size())
{
break;
}
}
if (match)
{
foundIndex = idx;
break;
}
}
if (!exact && foundIndex < 0)
{
// everything between 0..idx is revalidated
for (int i = 0; i <= Math.min(sortFields.size() - 1, foundIndex); i++)
{
Aast sortField = sortFields.get(i);
if (sortField.getType() == EXPRESSION)
{
sortField = (Aast) sortField.getFirstChild();
}
String sortName = (String) sortField.getAnnotation("name");
if (sortName.indexOf(".") != -1)
{
continue;
}
String qsortName = (String) sortField.getAnnotation("schemaname");
FieldInfo finfo = lookupFieldInfo(null, sortName);
if (finfo == null || !finfo.getQualified().equals(qsortName))
{
// something is wrong...
throw new SchemaException(String.format("Inconsistent index field found: %s %s %s!",
qsortName,
finfo == null ? "n/a" : finfo.getQualified(),
sortField.dumpTree(true)));
}
}
}
return foundIndex >= 0;
}
/**
* Multi-level lookup routine for processing searches on variable and
* function dictionaries based on
* the {@link com.goldencode.p2j.util.ScopedSymbolDictionary} class. All
* searches resolve to a symbol that exactly matches the passed key or to
* a built-in function or variable that is represented by a language
* keyword. The token type of the match is returned. Note that a token
* type of -1 is invalid in the Lexer and Parser, so this value is used to
* denote a search that found no match. Abbreviation processing is
* handled for built-ins ONLY and is naturally done by usage of the
* keyword dictionary in the first phase of the lookup process. The
* second lookup phase uses any returned keyword to modify the search or
* if no keyword is found, then a standard exact search is executed.
* <p>
* The algorithm checks for a keyword match. If not, a simple actual text
* lookup is done based on the exact text found in the source. If there is
* a keyword match, then if the keyword is reserved, the keyword's full
* text is used in the name lookup. The result would match the preloaded
* built-in name. If the keyword match is with an unreserved keyword,
* then the exact text from the source is used for a first lookup. If this
* does not succeed, then a second lookup using the full keyword text is
* used. This approach allows unreserved keywords to be used as user-
* defined names that hide built-in names if they exist. However the
* built-in names are still found if there is no user-defined name.
* <p>
* This usage of the full keyword text for lookup of built-ins is required
* since only the full text definition exists in the variable dictionary,
* rather than the full set of abbreviations. Instead the abbreviation
* matching is handled by the keyword dictionary and this method is aware
* of the difference.
*
* @param dict
* <code>ScopedSymbolDictionary</code> instance to search.
* @param actual
* Text string to match.
* @return The label's token type that was found or -1 if no match was
* found.
* @see #lookupVariable
* @see #lookupFunction
*/
private int lookupWorker(ScopedSymbolDictionary dict, String actual)
{
Keyword kw = lookupKeyword(actual);
int type = -1;
if (kw != null)
{
// the actual text matches a keyword
if (kw.isReserved())
{
// since this is a reserved keyword, this must be a
// built-in variable/function
type = lookupWorkerExact(dict, kw.getFullText());
}
else
{
// this is an unreserved keyword, this MAY be a
// built-in variable/function BUT we must allow a user-defined
// name to take precedence (note that if the actual text is
// is the same as the full keyword text, then we may still
// get a match here that is the built-in, assuming no
// user-defined name exists --> that is OK too)
type = lookupWorkerExact(dict, actual);
// check if there was a match to the actual text
if (type == -1)
{
// match the full text of the keyword with the built-in
type = lookupWorkerExact(dict, kw.getFullText());
}
}
}
else
{
// there is no keyword, so this must be a user-defined name
type = lookupWorkerExact(dict, actual);
}
return type;
}
/**
* Primary worker routine for processing searches on dictionaries based on
* the {@link com.goldencode.p2j.util.ScopedSymbolDictionary} class. All
* searches are for a symbol exactly matching the passed key. The token
* type of the match is returned. Note that a token type of -1 is invalid
* in the Lexer and Parser, so this value is used to denote a search that
* found no match. No abbreviation processing will be provided!
*
* @param dict
* {@code ScopedSymbolDictionary} instance to search.
* @param name
* Exact text string to match.
*
* @return The label's token type that was found or -1 if no match was found.
*/
private int lookupWorkerExact(ScopedSymbolDictionary dict, String name)
{
if (dict == null)
{
return -1; // frame not found because there is no frame dictionary in use
}
Integer result = (Integer) dict.lookupSymbol(name);
if (result != null)
{
return result;
}
return -1; // not found
}
/**
* Centralizes the process of setting an file-unique temporary index for
* each new wrapped object and then adding that object to the passed-in
* dictionary.
*
* @param ssd
* Symbol dictionary.
* @param global
* Add to global scope if <code>true</code>.
* @param name
* Variable name to add.
* @param wrapped
* Wrappered token object to add.
*/
private void addWrappedWorker(ScopedSymbolDictionary ssd,
boolean global,
String name,
TokenDataWrapper wrapped)
{
ClassDefinition cls = getCurrentClassDef();
if (cls != null)
{
if (wrapped instanceof Variable)
{
// inherit the temp-idx from the pre-scan - PRIVATE allows to match on everything
Variable var = (Variable) wrapped;
if ((ssd == varDict || ssd == dsetDict || ssd == dsrcDict) && !inMethod && !isPreScan())
{
// private will match any access mode
boolean st = var.isStatic();
Variable old = ssd == varDict
? cls.lookupVariableWrapper(name, KW_PRIVATE, st)
: ssd == dsetDict ? cls.lookupDataSetWrapper(name, KW_PRIVATE, st)
: cls.lookupDataSourceWrapper(name, KW_PRIVATE, st);
if (old != null)
{
var.setTempIndex(old.getTempIndex());
}
else
{
var.setTempIndex(cls.nextTempIdx());
}
}
else
{
var.setTempIndex(cls.nextTempIdx());
}
var.setClassDefinition(cls);
}
else
{
wrapped.setTempIndex(cls.nextTempIdx());
}
}
else
{
wrapped.setTempIndex(tempIdx);
tempIdx = tempIdx + 1;
}
if (ssd != null)
{
ssd.addSymbol(global, name, wrapped);
}
}
/**
* Given an AST which represents a field or variable which possibly has
* an extent, check for a subscript and return it. The assumed structure
* of the AST, if it does have an extent subscript is:
* <pre>
* FIELD/VAR
* |
* +--LBRACKET
* |
* +--EXPRESSION
* |
* +--NUM_LITERAL
* </pre>
* where the <code>FIELD/VAR</code> node is the <code>ast</code> parameter
* passed to this method.
* <p>
* Currently, only the <code>NUM_LITERAL</code> token type is supported
* for the subscript value. Any other type of expression will raise an
* exception.
*
* @param ast
* Field or variable AST.
*
* @return The 1-based subscript value from the <code>NUM_LITERAL</code>
* node depicted above, or 0 if there is no subscript.
*
* @throws UnsupportedOperationException
* if a token type other than a simple <code>NUM_LITERAL</code>
* is provided for the subscript expression.
* @throws IllegalArgumentException
* if the <code>NUM_LITERAL</code> token cannot be parsed into an
* integral value.
*/
private int getExtentSubscript(Aast ast)
{
int subscript = 0;
if (ast.getFirstChild() != null)
{
Aast lbracket = ast.getImmediateChild(LBRACKET, null);
if (lbracket != null)
{
Aast expr = lbracket.getChildAt(0);
Aast numlit = expr.getImmediateChild(NUM_LITERAL, null);
if (numlit == null)
{
numlit = expr.getImmediateChild(HEX_LITERAL, null);
if (numlit == null)
{
String err = "Non-NUM_LITERAL/non-HEX_LITERAL extent value not supported " +
"in this context!";
throw new UnsupportedOperationException(err);
}
String hex = numlit.getText();
try
{
decimal d = decimal.fromUnsignedHexLiteral(hex);
subscript = d.intValue();
}
catch (NumberFormatException exc)
{
throw new IllegalArgumentException("Invalid hex extent subscript: " + hex);
}
}
else
{
try
{
subscript = Integer.parseInt(numlit.getText());
}
catch (NumberFormatException exc)
{
throw new IllegalArgumentException(
"Invalid extent subscript: " + numlit.getText());
}
}
}
}
return subscript;
}
/**
* Convert any "." or "+" (internal .NET class) characters (in a Progress qualified class or
* interface name) to the current system's file name separator string.
*
* @param name
* Qualified class or interface name.
*
* @return Name that is safe to use with the current file system.
*/
private String convertSeparator(String name)
{
return name.replace('.', File.separatorChar).replace('+', File.separatorChar);
}
/**
* Search the PROPATH for a specific class or interface OR if this is a Java class, try to
* load it. For a non-Java class, the given name should be a Progress style qualified name
* (package + class). This will be converted to a file system name and searched for.
*
* @param name
* Qualified class or interface name.
* @param type
* The type of the using clause that is being checked. This
* changes the search order and may drop some paths from the
* search when possible.
* @param result
* Object in which to store the search results (the file system
* name, the built-in flag and the .NET flag) when the search
* is successful. If nothing is found, this object is unchanged.
*
* @return <code>true</code> if the class file was found.
*/
private boolean findFile(String name, UsingType type, SearchResult result)
{
if (type == UsingType.JAVA)
{
return isJavaClass(name, result);
}
// we tried an approach based on FileSystemDaemon.search() but it is
// slow, so we switched to a preloaded in-memory cache approach
name = convertSeparator(name);
Map<String, List<String>>[] searchList = null;
boolean[] builtin = new boolean[4];
boolean[] dotnet = new boolean[4];
// 4GL always matches case-insensitively (even on case-sensitive file system)
// the code below will differentiate using this var
boolean[] honorCase = new boolean[4];
// choose the search paths based on the type
switch (type)
{
case PROPATH:
// no need to search .NET stuff
searchList = new HashMap[2];
searchList[0] = propathCls;
searchList[1] = locate().oo4glCls;
builtin[0] = false; dotnet[0] = false; honorCase[0] = false;
builtin[1] = true; dotnet[1] = false; honorCase[1] = false;
break;
case DOTNET:
// the documentation says there is no need to search PROPATH but
// working code has found Progress.* using stmts with the FROM
// ASSEMBLY option, so we add the OO4GL stuff in as a last resort
// TODO: should this be more general?
searchList = new HashMap[3];
searchList[0] = locate().dotnetCls;
searchList[1] = locate().assemblyCls;
searchList[2] = locate().oo4glCls;
builtin[0] = true; dotnet[0] = true; honorCase[0] = caseSens;
builtin[1] = false; dotnet[1] = true; honorCase[1] = caseSens;
builtin[2] = true; dotnet[2] = false; honorCase[2] = false;
break;
default:
// search everything!
searchList = new HashMap[4];
searchList[0] = propathCls;
searchList[1] = locate().oo4glCls;
searchList[2] = locate().dotnetCls;
searchList[3] = locate().assemblyCls;
builtin[0] = false; dotnet[0] = false; honorCase[0] = false;
builtin[1] = true; dotnet[1] = false; honorCase[1] = false;
builtin[2] = true; dotnet[2] = true; honorCase[2] = caseSens;
builtin[3] = false; dotnet[3] = true; honorCase[3] = caseSens;
break;
}
WorkArea wa = locate();
Set<String> cvtSources = wa.cvtSources;
for (int i = 0; i < searchList.length; i++)
{
Map<String, List<String>> map = searchList[i];
String srch = honorCase[i] ? name : name.toLowerCase();
List<String> propathList = map.get(srch);
if (propathList == null)
{
// also try the other separator (Windows on Linux or Linux on Windows), this allows
// to define the search paths with both separators
srch = srch.replace(File.separatorChar, File.separatorChar == '/' ? '\\' : '/');
propathList = map.get(srch);
}
if (propathList != null)
{
if (propathList.size() == 1)
{
// Class previous found in oo4glCls, but also previous
// found in propath. In this case it should be removed
// from conversion.
if (builtin[i] && !dotnet[i] && result.filename != null)
{
File referenceFile = new File(result.filename);
if (ScanDriver.removeSource(referenceFile))
{
LOG.log(Level.WARNING,
String.format("Legacy builtin class %s found in conversion list, but is part of "
+ "FWD skeletons - this file will be removed from conversion.", srch));
}
}
result.filename = propathList.get(0);
result.builtin = builtin[i];
result.dotnet = dotnet[i];
if (builtin[i] && !dotnet[i])
{
break;
}
continue;
}
// multiple propath entries, we should take only that
// present in the conversion list.
if (!builtin[i] && !dotnet[i] && cvtSources != null)
{
for (String filename: propathList)
{
if (cvtSources.contains(filename))
{
result.filename = filename;
result.builtin = builtin[i];
result.dotnet = dotnet[i];
break;
}
}
// Log a warning that we could not the filename from multiple
// propath entries.
if (result.filename == null)
{
LOG.log(Level.WARNING,
String.format("Multiple entries found in PROPATH for %s, but none is in conversion list. "
+ "Please fix the PROPATH or the conversion list.", srch));
}
}
}
}
if (result.filename != null)
{
// Source found in propath, should be added to the conversion list.
if (!result.builtin && !result.dotnet && !cvtSources.contains(result.filename))
{
String srch = name.replace(File.separatorChar, '.');
if (loadClassDefinition(this, srch) == null)
{
File referenceFile = new File(result.filename);
ScanDriver.addSource(referenceFile);
}
}
return true;
}
return false;
}
/**
* For 4GL, .NET or unspecified types, search the PROPATH for the given partially qualified
* class or interface. For Java types, a non-propath search is made.
*
* @param name
* Partially qualified class or interface name.
* @param type
* The type of the using clause that is being checked. This
* changes the search order and may drop some paths from the
* search when possible.
* @param result
* Object in which to store the search results (the file system
* name, the built-in flag and the .NET flag) when the search
* is successful. If nothing is found, this object is unchanged.
*
* @return <code>true</code> if the class file was found.
*/
private boolean srchPropathFindFile(String name, UsingType type, SearchResult result)
{
if (propath != null && type != UsingType.JAVA)
{
for (int i = 0; i < propath.length; i++)
{
String fqn = null;
// deal with special case of solo dot
fqn = (propath[i].equals(".")) ?
name : Configuration.normalizeFilename(propath[i] + File.separatorChar + name);
// since we are dealing with a class or interface, ditch the leading path indicators
if (fqn.startsWith("." + File.separator))
{
fqn = fqn.substring(2);
}
if (findFile(fqn, type, result))
{
return true;
}
}
}
else if (type == UsingType.JAVA)
{
return findFile(name, type, result);
}
return false;
}
/**
* Test if the given package exists in the PROPATH, or in the 4GL system packages or in the
* .NET system namespaces.
*
* @param pkg
* Package name to search for.
*
* @return The normalized OS path of this package.
*/
private String pkgExists(String pkg)
{
pkg = convertSeparator(pkg);
File file = null;
String path = null;
for (int i = 0; i < propath.length; i++)
{
if (propath[i].length() == 0 || ".".equals(propath[i]))
{
// check a relative file in the current directory
file = new File(pkg);
}
else
{
// check a relative file in the specified path
file = new File(propath[i], pkg);
}
// 4GL classes are always matched case-insensitively (even on case-sensitive
// file systems
path = exists(file, false);
if (path != null)
{
path = Configuration.normalizeFilename(path);
return Configuration.normalizeFilename(Configuration.getParameter("basepath"), path);
}
}
// check if the package exists as part of the 4GL system
file = new File(oo4glPath, pkg);
path = exists(file, false);
// 4GL classes are always matched case-insensitively (even on case-sensitive file systems
if (path != null)
{
return Configuration.normalizeFilename(oo4glPath, path);
}
// check if the namespace for the system classes in .NET
file = new File(dotnetPath, pkg);
path = exists(file, caseSens);
if (path != null)
{
return Configuration.normalizeFilename(dotnetPath, path);
}
// check if the namespace for the user assemblies in .NET
file = new File(ASSEMBLY_PATH, pkg);
path = exists(file, caseSens);
if (path != null)
{
return Configuration.normalizeFilename(ASSEMBLY_PATH, path);
}
return null;
}
/**
* Implements a check for the file's existence while honoring the
* case-sensitive file-system flag. If the original file-system was
* case-sensitive, then a simple <code>exists()</code> method call is
* made on the file object. Otherwise a case-insensitive search is
* undertaken.
*
* @param file
* The file to check.
* @param caseSens
* <code>true</code> if the legacy system had a case-sensitive
* file-system.
*
* @return The file's canonical path.
*/
private String exists(File file, boolean caseSens)
{
// quick out: does the exact match exist?
if (file.exists())
{
try
{
return file.getCanonicalPath();
}
catch (IOException e)
{
// ignore
}
}
if (!caseSens)
{
// TODO: this is broken!!!! It was found to fail when the exact case-sensitive match
// actually existed (resolved by checking for that first, above). It is not
// known if it fails in other cases too.
return FileSystemDaemon.getCaseInsensitiveMatch(file, true);
}
return null;
}
/**
* Map the keyword type to the corresponding field type.
*
* @param ktype
* Keyword type.
*
* @return The matching field type or -1 if the input is invalid.
*/
private int keywordToFieldType(int ktype)
{
int ftype = -1;
switch (ktype)
{
case KW_BLOB:
ftype = FIELD_BLOB;
break;
case KW_CHAR:
ftype = FIELD_CHAR;
break;
case KW_CLASS:
case CLASS_NAME:
ftype = FIELD_CLASS;
break;
case KW_CLOB:
ftype = FIELD_CLOB;
break;
case KW_COM_HNDL:
ftype = FIELD_COM_HANDLE;
break;
case KW_DATE:
ftype = FIELD_DATE;
break;
case KW_DATETIME:
ftype = FIELD_DATETIME;
break;
case KW_DATE_TZ:
ftype = FIELD_DATETIME_TZ;
break;
case KW_DEC:
ftype = FIELD_DEC;
break;
case KW_HANDLE:
ftype = FIELD_HANDLE;
break;
case KW_INT:
ftype = FIELD_INT;
break;
case KW_INT64:
ftype = FIELD_INT64;
break;
case KW_LOGICAL:
ftype = FIELD_LOGICAL;
break;
case KW_RAW:
ftype = FIELD_RAW;
break;
case KW_RECID:
ftype = FIELD_RECID;
break;
case KW_ROWID:
ftype = FIELD_ROWID;
break;
case KW_WID_HAND:
ftype = FIELD_HANDLE;
break;
}
return ftype;
}
/**
* Process a request for the schema dictionary. If that request cannot be satisfied from
* the current schema dictionary instance AND the current scope is processing a class, then
* the schema dictionaries of the parent hierarchy will be checked until the request is
* satisfied or there are no more parents in the inheritance chain.
*
* @param worker
* The core processing to be executed on the given schema dictionary.
* @param continu
* A condition which if it returns <code>true</code>, then the next schema
* dictionary should be tried. On <code>false</code>, the current result is
* returned.
*
* @return The result of the processing.
*/
private <V> V processHierarchy(SchemaHelper<V> worker, Predicate<V> continu)
throws SchemaException
{
SchemaException[] exc = new SchemaException[1];
V result = worker.process(schemaDict);
final V backup = result;
// the predicate determines if we continue searching
if (continu.test(result))
{
ClassDefinition def = getCurrentClassDef();
if (def != null)
{
java.util.function.Function<ClassDefinition, V> func = (ClassDefinition cls) ->
{
V value = backup;
SchemaDictionary dict = cls.getSchemaDict();
// only test this class if there is something to test
if (dict != null)
{
try
{
// try again
value = worker.process(dict);
}
catch (SchemaException e)
{
exc[0] = e;
}
}
return value;
};
result = def.calcFromParentGraph(func, continu, backup, true, false);
if (exc[0] != null)
{
throw exc[0];
}
}
}
return result;
}
/**
* Return the access mode as encoded by the AST type.
*
* @param am
* The access mode node.
*
* @return The type of the node or KW_PRIVATE if there is no node.
*/
private int decodeAccessMode(Aast am)
{
return (am != null) ? am.getType() : KW_PRIVATE;
}
/**
* Return the static mode as encoded by the AST type.
*
* @param st
* The static mode node.
*
* @return <code>true</code> if the type of the node KW_STATIC.
*/
private static boolean decodeStatic(Aast st)
{
return (st != null && st.getType() == KW_STATIC);
}
/**
* Calculates the access mode for method and data member searches of
* class definitions, based on the presence and contents of the given
* class name and the current context. By default the access mode will
* be <code>KW_PUBLIC</code>. If this request is made within the context
* of a class definition, then the access mode will be
* <code>KW_PRIVATE</code> if <code>cname</code> is <code>null</code> or
* if <code>cname</code> is the same as the current class definition
* name. If both the current class and the searched one are in the same package,
* the access mode will be <code>KW_PK_PRIV</code>. The access mode will
* be <code>KW_PROTECTD</code> if the <code>cname</code> is not <code>null</code>
* and it is the equivalent of one of the parent class names in the current class
* definition's inheritance chain.
*
* @param cname
* Fully qualified class name being searched or <code>null</code>
* if the current class definition is to be searched.
*
* @return The calculated access mode of <code>KW_PUBLIC</code>,
* <code>KW_PROTECTD</code>, <code>KW_PK_PRIV</code> or <code>KW_PRIVATE</code>.
*/
private int calcAccessMode(String cname)
{
int access = KW_PUBLIC;
ClassDefinition cls = getCurrentClassDef();
if (cls != null)
{
if (cname != null)
{
String current = cls.getName();
if (cname.equalsIgnoreCase(current))
{
// inside a class def and explicitly referencing the current
// class, this is a private search
access = KW_PRIVATE;
}
else
{
if (getLegacyPackageName(current) != null &&
getLegacyPackageName(cname) != null &&
getLegacyPackageName(current).equalsIgnoreCase(getLegacyPackageName(cname)))
{
access = KW_PK_PRIV;
}
Predicate<ClassDefinition> check = (ClassDefinition def) ->
{
return cname.equalsIgnoreCase(def.getName());
};
if (cls.testParentGraph(check))
{
// this is explicit access to a parent class from within
// a subclass definition, this is a protected search
access = KW_PROTECTD;
}
}
}
else
{
// we are inside a class def and have no explicit class to
// search, this is a private search of the current class'
// hierarchy
access = KW_PRIVATE;
}
}
return access;
}
/**
* Extracts the legacy package name from a fully qualified class name.
* <p>
* If the class name is {@code null} or empty, this method returns {@code null}.
* If the class name does not contain a dot (i.e., is not in a package),
* this method returns an empty string.
* Otherwise, it returns the substring before the last dot, which represents the package name.
*
* @param className
* The fully qualified class name.
* @return The package portion of the class name, an empty string if no package exists,
* or {@code null} if the input is {@code null} or empty.
*/
public static String getLegacyPackageName(String className)
{
if (className == null || className.isEmpty())
{
return null;
}
int lastDot = className.lastIndexOf('.');
return (lastDot >= 0) ? className.substring(0, lastDot) : "";
}
/**
* Throws runtime exception or returns an error message to inform that the class of the supplied qualified
* name was not found in PROPATH.
*
* @param qname
* Class qualified name.
* @param throwEx
* The method will throw a runtime exception when <code>true</code>, otherwise it will return
* the formatted error message.
*
* @return The formatted error message
*/
private String reportClassNotFound(String qname, boolean throwEx)
{
String err = String.format("Cannot find class/interface %s in PROPATH.", qname);
if (throwEx)
{
throw new RuntimeException(err);
}
return err;
}
/**
* Create a new {@link Variable}, with the specified details.
*
* @param name
* The variable legacy name.
* @param tokenType
* The type of the variable (which can be any kind of member for a legacy class).
* @param node
* The defining node for this variable
* @param isStatic
* Flag indicating that this is a static definition.
*
* @return See above.
*/
static Variable createVariable(String name, int tokenType, Aast node, boolean isStatic)
{
Variable dataset = new Variable(name, tokenType, null);
dataset.setOptions(node);
dataset.setStatic(isStatic);
return dataset;
}
/**
* Describes the type of the USING specification.
*/
private static enum UsingType
{
PROPATH, DOTNET, JAVA, UNSPECIFIED
};
/**
* Functional interface for use with lambdas.
*/
private interface SchemaHelper<V>
{
/**
* Execute the logic against the given instance of the schema dictionary.
*
* @param dict
* The schema dictionary.
*
* @return The resulting value generated.
*/
public V process(SchemaDictionary dict)
throws SchemaException;
}
/**
* Store the data in a USING statement.
*/
private static class UsingSpec
{
/** Match specification (package/.NET namespace or full classname). */
private String spec = null;
/** Match type. */
private UsingType type = UsingType.UNSPECIFIED;
/**
* Primary constructor.
*
* @param spec
* Match specification (package/.NET namespace or full
* classname).
* @param type
* Match type.
*/
public UsingSpec(String spec, UsingType type)
{
this.spec = spec;
this.type = type;
}
}
/**
* Store the data resulting from a given file system search.
*/
private static class SearchResult
{
/** File system object that was found (includes pathing). */
private String filename = null;
/** Fully qualified class or interface name relative to PROPATH. */
private String clsname = null;
/** Flag to mark a built-in Progress (OO 4GL) class. */
private boolean builtin = false;
/** Flag to mark a .NET class. */
private boolean dotnet = false;
/** Flag to mark a Java class. */
private boolean java = false;
/** Search hint. */
private UsingType type = UsingType.UNSPECIFIED;
}
/**
* Container with context-local data.
*/
private static class WorkArea
{
/**
* Flag indicating that {@link SymbolResolver#loadClass} will throw an exception if it can't find the
* class, used by {@link SymbolResolver#tryLoadClass}.
*/
private boolean forceLoadFailure = false;
/** Flag indicating that the legacy builtin classes have been loaded. */
private boolean legacyClassesLoaded = false;
/**
* A cache of class definitions previously converted (not by the current run). The key is
* the qualified class name.
*/
private Map<String, ClassDefinition> classCache = new HashMap<>();
/** Mapping of class files to their qualified name. */
private Map<String, String> fname2qname = new HashMap<>();
/** Mapping of builtin class qualified name to its Java converted name. */
private Map<String, String> legacyToJava = null;
/**
* Holds class dictionary for each used PROPATH. This allows to limit the visible classes
* only to the ones in the current PROPATH (a performance penalty will exist, as each class
* may be loaded in every distinct PROPATH in the project).
*/
private Map<String, ScopedSymbolDictionary> propathClassDicts = new HashMap<>();
/** 4GL sources defined in the conversion list */
private Set<String> cvtSources = null;
/** Classes in the OO4GL_PATH, mapped qualified classname to filename. */
private Map<String, List<String>> oo4glCls = null;
/** Classes in the DOTNET_PATH, mapped qualified classname to filename. */
private Map<String, List<String>> dotnetCls = null;
/** Classes in the ASSEMBLY_PATH, mapped qualified classname to filename. */
private Map<String, List<String>> assemblyCls = null;
/**
* The nesting level of {@link SymbolResolver#loadClass} calls when performing 1st-level
* load of a class.
*/
private int preScanPass = 0;
/** The set of class/interface references processed during 1st level parse of a class. */
private Set<String> preScanRefs = "TRUE".equalsIgnoreCase(Configuration.getParameter("case-sensitive"))
? new HashSet<>()
: new CaseInsensitiveHashSet<>();
/** An ordered set of class/interface references processed. */
private LinkedList<String> processed = new LinkedList<>();
/** The converter to generated Java names. */
private NameConverter nconvert = new NameConverter();
/** Set of pending classes to be loaded. */
private HashSet<String> pendingLoading = new HashSet<>();
}
}