SourceNameMapper.java
/*
** Module : SourceNameMapper.java
** Abstract : Progress procedure file name to Java class name run-time mapping (bidirectional)
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 SIY 20050817 @22156 Created initial version.
** 002 GES 20060124 @24072 Rewrite to move to a directory based approach for storage of the
** mapping data. In addition, the mapping data used at runtime is
** stored in context-local instances to ensure that multiple
** configurations can be run simultaneously. Added a reverse mapping
** from Java to Progress names.
** 003 GES 20060201 @24185 Added external init option so that the class can be initialized from
** a source other than the directory.
** 004 NVS 20070126 @32009 "class-mapping" node of the directory is searched as an account
** level standard search item.
** 005 ECF 20070220 @32186 Made map access thread-safe. Synchronized initialization of mapping
** data with read and write requests. This fixes a race condition
** which was resulting in an NPE.
** 006 GES 20080722 @39167 Added a fallback for convert() to allow a parameter which is already
** an existing fully qualified class name, to be returned. Major
** rewrite to load the name mapping data from a JAR file resource. The
** new resource has a much simpler format.
** 007 SVL 20090310 @41448 Keys into methodMap for the Progress proc/function name to Java
** name mapping are always lower-cased.
** 008 GES 20090515 @42235 Import change.
** 009 ECF 20101004 Handle unqualified file name references in convertName method.
** 010 CA 20121220 Reworked the mapping to include the new emitted info (by
** collect_names.rules), needed by the
** persistent and super procedures implementation.
** 017 CA 20130209 Added dynamic EXTENT and EXTENT parameter support.
** 018 CS 20130304 Added support for checking if a class exists in a
** certain package as result of conversion from file
** 019 EVL 20130412 Fixing the regular expressions for windows runtime in
** initMappingData() method to properly handle windows file separator.
** 020 EVL 20130529 Replacing getLegacySearchPath() with getSearchPath() to obtain
** full effective PROPATH variable value to reflect the changes in
** PROPATH approach implementation.
** 021 CA 20130813 Added constants for the parameter modes.
** 022 CA 20130925 Added support for PUBLISHED-EVENTS attribute.
** 023 CA 20131116 Added support for the FUNCTION ... MAP TO clause.
** 024 GES 20131218 Rework to move InternalEntry, Parameter and other logic/data out
** into separate classes. This makes the overall code easier to
** understand and more extendable. Refactored some methods to use
** common code. Completed native API support.
** 025 CA 20140218 Changes related to PROGRAM-NAME implementation: added
** convertJavaMethod to resolve a Java method name to a legacy name.
** Removed convertStackEntry, as implemenetation was moved to
** ProcedureManager.
** 026 EVL 20140310 The canonicalize() and convertNames() methods have been modified to
** take into account possible usage of the \ file separator in Windows
** for program name. Currently in run-time case the file separator has
** constant value of "/". All Windows separators will be replaced with
** this one. For Windows the convertNames() method handles the absolute
** file names with drive letter included and relative file names. For
** this support the properly configured searchpath value in server's
** directory.xml file is required. But the existence of the 4GL source
** code is not expected on the target system. The canonicalize() method
** was removed from the code part where is does nothing useful(because
** the input parameters name already "canonicalized").
** 027 ECF 20140913 Reduced memory footprint.
** 028 ECF 20150424 Minor memory tweak.
** 029 ECF 20150601 Minor changes to accommodate InternalEntry changes. Removed some
** unnecessary intern()'ing of strings.
** 030 OM 20150710 Added support for obtaining the type of a parameter.
** 031 ECF 20150805 Refactored synchronization to improve throughput. Replaced
** StringBuffer with StringBuilder.
** 032 CA 20160418 Added API to resolve an internal-entry (or external program) list of
** parameters.
** 033 OM 20160421 Identified 4GL source searched by their legacy absolute path. Added
** generics. Javadoc fixes. foreach iterations.
** 034 OM 20160527 SourceNameMapper.convertName() was hidden and exposed two methods that
** return converted class-name and legacy file name.
** 035 OM 20160711 Added MEMPTR & RAW support in getTypeForParam() method.
** 036 CA 20161020 Fixed case-insensitive resolution with .r (rcode) ext program names.
** 037 GES 20171204 Improved comments and formatting.
** 038 CA 20180216 Reworked convertName - 4gl-src-root will provide a list of absolute
** root paths which will be matched against the program name and against
** the propath entries.
** Also, it fixes resolution so that the search is done first via the:
** - absolute name
** - PROPATH
** - direct match.
** 039 CA 20180504 Added support for folder aliasing: these are specified via the
** "path-aliases" per-server directory node, and provide a mapping of
** alias folder name to their 'source' counterpart. Also, added support
** to identify the target source program for a RUN program.r statement.
** 040 SBI 20180518 Renamed file-system/pkgroot and file-system/4gl-src-root into
** legacy-system/pkgroot and legacy-system/4gl-src-root.
** 041 CA 20180727 Added getPackageRoot.
** 042 CA 20180801 If the legacy program name starts with '.', then use this name in
** convertName - 4GL doesn't change the legacy name, as the lookup is
** always done in current folder.
** 043 CA 20181029 Moved code from getTypeForParam to BDT.fromTypeName.
** 044 CA 20181216 Fixed loading OO methods, constructors and destructors.
** 045 CA 20190122 Runtime support for legacy 4GL classes.
** CA 20190219 Enhanced so that legacy 4GL classes are managed via LegacySignature
** annotations - name_map.xml is just a registry for the classes.
** Builtin legacy 4GL classes are registered on-the-fly, when they are
** first accessed.
** ECF 20190323 Reimplemented InternalEntry list as a map for faster searches.
** 045 CA 20190520 buildLegacyClass will register only OO method, ctor or dtor.
** CA 20190526 Fixed runtime and conversion support for legacy class properties used
** as OUTPUT or INPUT-OUTPUT arguments.
** 046 CA 20190611 Added support for legacy services (REST).
** 047 CA 20190703 Added support for WebHandler services.
** 048 OM 20190508 Avoid NPE, added DATASET support to name_map file. Logged missing
** xml data file event.
** 049 CA 20190711 Added API to explicitly load the legacy services.
** CA 20190717 The OO accessor or method must be looked in super-classes, too.
** 050 CA 20200412 Added incremental conversion support.
** 051 CA 20200514 Emit the LegacySignature at the external program's 'execute' method and all its
** internal entries.
** Added support for SOAP web services.
** CA 20200605 Added a lazy approach of resolving the Java method(s) for the entries in an
** external program - the first access of InternalEntry.getMethod() will load the
** class and resolve the Java methods for all internal entries.
** RFB 20200706 Modified convertName to handle the situation with a procedure name not passed in
** with an extension, which works fine in 4GL if it is backed by a .r, .p, or .w.
** RFB 20200730 Moved pathSep and caseSens to the WorkArea so that they can be kept separate
** per session. Also had to duplicate the mappings to include both case-sensitive
** and case-insensitive lists. Only one or the other was loaded, depending upon
** whether client or server token is loaded first, when one is on Linux, the other
** on Windows. This is critical when multiple project-tokens are used.
** ECF 20200916 Avoid context-local lookups when possible.
** CA 20200927 Cache failed Class.forName strings, to avoid the lookup overhead.
** CA 20200930 Cache the InternalKey hashcode.
** OM 20210203 Avoided NPE and proper logging in initMappingData().
** CA 20210221 Default to Progress.Lang.Object if an 'object' parameter or method is missing its
** qualified annotation.
** CA 20210609 Added the extent for properties/variables (where it applies).
** CA 20210628 Fixed convertName and others - the Windows OS must be checked for the legacy
** platform under which the application ran, and not the running FWD client's OS.
** HC 20211102 Improved error handling during loading of name mapping data.
** CA 20220208 Added findLegacyEntryName, a helper to resolve the legacy name for a Java method
** defined in class, when only the method's name is known.
** OM 20220316 Added awareness for 'altFileSep' separator found in ABL paths.
** CA 20220329 Added support for REST services written directly in Java.
** CA 20220420 REST services written directly in Java can be configured via the 'rest-service'
** element.
** TJD 20220822 Prevent using uninitialized mapping data
** TJD 20220823 Prevent recursive calls to initMappingData() workaround
** TJD 20220824 Prevent recursive calls to initMappingData() workaround in more effective way
** SBI 20221215 Filter the same REST java methods having erased parameters types.
** CA 20220520 Initialize SourceNameMapper at server startup.
** The constructor (static or instance) and destructor lookup now relies on the
** LegacySignature annotation to resolve them, instead of the Java method name -
** this is required for ctor overload support at conversion and runtime.
** CA 20220606 Added lazy loading of legacy converted classes. As SourceNameMapper is
** initialized at server startup, the 'initMappingData' calls were removed from the
** APIs.
** CA 20220609 Fixed a regression in destructor processing.
** 052 HC 20230118 Eliminated some of the uses of String.toUpperCase and/or
** String.toLowerCase for performance.
** HC 20230120 The case insensitive maps reimplemented with CaseInsensitiveHashMap.
** HC 20230130 Improved performance of the methods removeUpPath and canonicalize.
** Replaced some occurrences of split and replaceAll with pre-compiled
** regular expression patterns.
** Introduced caching for some session context lookups.
** Improved class lookup in the method convertName.
** Replaced a use of HashMap with CaseInsensitiveHashMap.
** 053 CA 20230221 Javadoc fixes.
** 054 CA 20230322 Cache the suffix pattern used by 'getOverloads'.
** 055 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 056 CA 20230724 Resolve and cache the 'execute methods' at LegacyClass.executeMethods.
** 057 ME 20230829 Added support for bind parameter flag.
** ME 20230919 Add new public buildInternalEntry method (used by reflection).
** 058 CA 20231009 Resolving a legacy method needs to exclude super-class overridden methods.
** 059 CA 20231031 The constructor and destructor 'internal entry name' is the exact legacy name
** for this entry, as it is reported by PROGRAM-NAME and error callstack.
** 060 CA 20231030 Added overloads/overrides support for interfaces.
** 061 DDF 20230926 Lookup the unqualified program name firstly when converting a progress
** file name to a java class name.
** DDF 20231002 Short-circuit files that have extensions and do not match any extensions
** of the existent legacy program files. Added r-code file searching.
** DDF 20231020 caseSens value of the WorkArea should be taken into consideration when
** searching r-code files, removed unnecessary local.get() call in
** convertName() and made code adjustments.
** DDF 20231102 Implemented a cache for search-specs found during conversion.
** DDF 20231107 Improved the cache implementation and used of AssociatedThread
** to create new cache entries for propaths.
** DDF 20231110 Improved cache, store additional entries besides path-mappings.
** DDF 20231113 Removed SOURCE_CACHE_SIZE and made the sourceCache configurable
** through CacheManager. Replaced ExecutorService usage with a normal
** thread when initializing the path-mappings map.
** DDF 20231113 Even if there are no path-mappings, we should still allow the
** sourceCache to be initialized and populated with search-specs
** during runtime.
** DDF 20231116 Fixed NPE when there are no path-mapping nodes in name_map.xml.
** DDF 20231117 Cached searchPath and propath hash code.
** DDF 20231117 Synchronized searchPathCache.
** DDF 20231121 Changed lock/synchronization for sourceCache, initializationPaths
** and convertedNames.
** DDF 20231128 Replaced the Executor with a ThreadPoolExecutor.
** DDF 20231205 Modified mappingExecutor, added SourceThreadFactory.
** DDF 20231208 Created MonitorRunnable to avoid a source cache entry from
** creating a task twice. Made searchPathCache configurable.
** DDF 20231212 Remove identifier after source cache entry is created.
** 062 CA 20231213 The synthetic 'execute' methods emitted for legacy classes can be dropped if there
** is nothing emitted in them. These Java methods are also marked with Type.EXECUTE
** LegacySignature annotation, and their BlockManager API removed - the FWD runtime
** will run this only once.
** 063 CA 20231223 The resolved name cache must be written/created only after the initialization
** finished (pathMappingInitialized is true).
** 'createSourceCacheEntry' must not not process (or overwrite) the same propath hash.
** 064 SB 20231218 Added getter functions for the setters and getters found in ClassMap.
** 065 AD 20240125 The search for a Java class name from a 4GL procedure name can now use a given
** list of directories instead of searching in the PROPATH by default.
** 066 GBB 20240304 LegacyService annotation not found logged at lower level.
** 067 GBB 20240404 Support for multiple name_map.xml files.
** 068 OM 20240430 Allow name_map.xml files to be loaded also from file system (developer mode).
** 069 GBB 20240516 Fix for comparing resolved programs in multiple name_map.xml
** (ooname read as empty, but saved as null).
** 070 DDF 20240514 Fix NPE when the legacy OO type received as a parameter is an interface, which
** does not have a superclass (findLegacyPropertyExtent and findLegacyAccessor).
** 071 ICP 20240705 Fixed method of obtaining the class loader.
** 072 CA 20240809 Cache the legacy method name as an interned strings and move the map to an
** identity map, to avoid lowercase keys.
** 073 VVT 20240826 convertJavaProg() renamed. See #8613-16.
** 074 AP 20250127 Added method to check whether a class is legacy or converted.
** 075 PBB 20250509 Modified implementation of DYNAMIC-PROPERTY function to introduce support for
** DYNAMIC-INVOKE builtin function that returns or might return an extent.
*/
/*
** 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.util;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.*;
import java.util.logging.*;
import java.util.regex.*;
import java.net.*;
import com.goldencode.p2j.classloader.*;
import com.goldencode.p2j.util.logging.*;
import org.w3c.dom.*;
import com.goldencode.util.*;
import com.goldencode.p2j.util.InternalEntry.Type;
import com.goldencode.cache.CapacityPolicy;
import com.goldencode.cache.LRUCache;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.library.*;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.persist.CacheManager;
import com.goldencode.p2j.rest.*;
import com.goldencode.p2j.soap.*;
import com.goldencode.p2j.security.*;
import org.xml.sax.*;
import javax.xml.parsers.*;
/**
* This class performs a bidirectional run-time mapping of a Progress
* procedure file name to and from a Java class name. It also supports
* run-time translation of Progress internal procedure names into Java
* method names.
* <p>
* <b>The mapping data is loaded from the directory into data structures that
* are <code>static</code>. This means that at this time this mapping data
* is global to the entire server rather than being context-local. The
* reason for this is to reduce the amount of memory used for this data
* since 2+ map entries exist for each original source file (thus these maps
* each contain thousands of entries for any reasonably sized
* application).</b>
* <p>
* <b>The current implementation assumes a single root path for all Progress
* procedure file names and single base package path for the generated Java
* classes. Despite these assumptions it is possible to translate
* Progress procedure names incorrectly (to a Java class name which does not
* belong to the subtree specified by the single root or which does not
* belong to the base package path). These problems are caused by using
* mismatching absolute paths for Progress names or names which start with
* a dot ('.') for Java class names.</b> The leading dot is stripped
* from the returned class name and is not returned as a result of a
* mapping.
* <p>
* The source file name can be in one of the many forms: relative path,
* absolute path, can contain references to current and to upper level
* directories. Also, the source file name can use the file naming format of
* the original operating system used to run the Progress program rather than
* the current operating system used to run the converted Java application.
* To this the directory contains values that define the original source
* system's parameters such as path delimiter, file name delimiter and case
* sensitivity.
*/
public class SourceNameMapper
{
/** Constant identifying no-extent parameters. */
public static final int NO_EXTENT = -2;
/** Constant identifying dynamic-extent parameters. */
public static final int DYNAMIC_EXTENT = -1;
/** The name of the file containing mappings. */
private static final String NAME_MAP_FILE = "name_map.xml";
/** The overload suffix pattern used by {@link #getOverloads}. */
private static final Pattern OVERLOAD_SUFFIX = Pattern.compile("_[0-9]+");
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(SourceNameMapper.class);
/** Context-local data. */
private static final ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
{
public WorkArea initialValue()
{
WorkArea wa = new WorkArea();
EnvironmentOps.addSourcePathListener(SourceNameMapper::setSearchPath);
wa.isWindows = EnvironmentOps.isLegacyPlatformWindows();
return wa;
}
};
/**
* Set of {@link Class#forName} lookups by {@link #convertName} which resolve (or not) to a Java class for
* the given program name.
*/
private static final Map<String, Boolean> CLASS_LOOKUPS = new ConcurrentHashMap<>();
/** Rest service mappings element. */
private static final String ELEM_REST_SERVICE = "rest-service";
/** Class mappings element. */
private static final String ELEM_CMAP = "class-mapping";
/** Methods mappings element. */
private static final String ELEM_MMAP = "method-mapping";
/** Path mappings element. */
private static final String ELEM_PMAP = "path-mapping";
/** Parameter info element. */
private static final String ELEM_PARAM = "parameter";
/** Progress name attribute. */
private static final String ATTR_PNAME = "pname";
/** Java name attribute. */
private static final String ATTR_JNAME = "jname";
/** Filename name attribute. [<path-to-file>/file] */
private static final String ATTR_PATHNAME = "pathname";
/** TYPE attribute. */
private static final String ATTR_TYPE = "type";
/** LIBNAME attribute. */
private static final String ATTR_LIBNAME = "libname";
/** MAP-TO attribute. */
private static final String ATTR_MAP_TO = "map-to";
/** Parameter MODE attribute. */
private static final String ATTR_PARAM_MODE = "mode";
/** Named events attribute. */
private static final String PUBLISHED_EVENTS = "published-events";
/** 'with-services' attribute. */
private static final String WITH_SERVICES = "with-services";
/** Qualified OO name attribute. */
private static final String OONAME = "ooname";
/** Counter for the number of active threads populating {@link #initializationPaths}. */
private static final AtomicInteger PATH_INIT_THREADS_COUNTER = new AtomicInteger(-1);
/**
* Counter value that is used when adding new values to {@link #initializationPaths}.
* The value is modified when reading the initial path-mapping nodes and whenever
* a new entry is added to an existent cache.
*/
private static final AtomicInteger MAPPING_COUNTER = new AtomicInteger(0);
/** Map of aliases (r-code folders) to their source folders (conversion folders). */
private static Map<String, String> pathAliases = null;
/** Map of aliases (r-code folders) to their source folders (conversion folders). - case-insensitive */
private static Map<String, String> pathAliases_ci = null;
/** Progress procedure file name to Java class name map table. */
private static volatile Map<String, ExternalProgram> p2jMap = null;
/** Progress procedure file name to Java class name map table (case-insensitive). */
private static volatile Map<String, ExternalProgram> p2jMap_ci = null;
/** Map of program names without extension to the programs which can be resolved with this name. */
private static volatile Map<String, List<ExternalProgram>> p2jNonExtMap = null;
/** Map of program names without extension to the programs which can be resolved with this name (ci). */
private static volatile Map<String, List<ExternalProgram>> p2jNonExtMap_ci = null;
/** Map of lookup instances for unqualified program names. */
private static Map<String, PathLookup> pathLookups = null;
/** Progress Java class name to procedure file name map table. */
private static Map<String, ExternalProgram> j2pMap = null;
/** Cache of preprocessed mapped paths to target procedures. */
private static LRUCache<Long, SourceNameMapperCache> sourceCache = null;
/** Search-spec paths found during conversion and read from name_map.xml. */
private static volatile Map<String, Integer> initializationPaths = null;
/** Set of file extensions that were found for all procedure file name nodes */
private static Set<String> allowedLegacyExtensions = null;
/**
* Cache of searchPaths to a String array containing the resulted propath and originalPrograpth.
* The order is: propath and originalPropath.
*/
private static LRUCache<String, String[][]> searchPathCache = null;
/** The list of programs exposed as services (the 'with-services' attribute or 'rest-service' nodes). */
private static List<ExternalProgram> servicePrograms = null;
/** Association between converted legacy 4GL programs to their defined internal entries. */
private static volatile Map<Class<?>, LegacyClass> classMap = null;
/**
* The 4GL legacy roots for source files. Used for detecting full-paths file names.
* Each entry identifies a project which is run on the current JVM.
*/
private static String[] legacyRoots = null;
/** Base path for all Java packages. */
private static String pkgroot = null;
/** Custom package to init data for. */
private static String custpkgroot = null;
/** Base path for Progress 4GL code. */
private static String proroot = null;
/** Mapping data initialization completion indicator */
private volatile static boolean mappingDataInitialized = false;
/** Mapping data initialization in progress indicator */
private volatile static boolean mappingDataInitInProgess = false;
/**
* Name element separator used for the Progress file name data. At runtime this is OS
* independent value used internally in name_map.xml file to resolve procedure names.
*/
private static String fileSep = "/";
/** Character form of fileSep */
private static char fileSepChar = '/';
/** Regular expression pattern for fileSep field */
private static final Pattern fileSepRegexp = Pattern.compile("/");
/** Double backslash precompiled regular expression pattern */
private static final Pattern dblBackslashRegexp = Pattern.compile("\\\\");
/**
* Alternative name element separator used for the Progress file name data. If the path is received from
* Windows ABL code, it may contain a mix of both {@code fileSep} and this.
*/
private static final String altFileSep = "\\";
/**
* Executor for operations that build source cache entries.
* Threads should not be kept idle since it is possible for it to keep the same
* SecurityContext when it should be different.
*/
private static final ExecutorService mappingExecutor =
new ThreadPoolExecutor(0,
1,
0,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new SourceThreadFactory());
/**
* Initialize all the maps - this can be time-consuming, as the legacy OO classes are initialized from the
* Java annotations.
*/
public static void initialize()
{
initMappingData();
}
/**
* Register all exposed legacy services.
*/
public static void registerServices()
{
if (servicePrograms == null)
{
return;
}
for (ExternalProgram ep : servicePrograms)
{
registerServices(ep, addPkg(ep.jname));
}
}
/**
* Given a program name, replace all folder names which have an associated 'source' folder,
* and return the new path.
*
* @param pname
* The legacy program name.
*
* @return The resolved program names or empty array if no change was possible.
*/
public static String[] resolvePathAliases(Map<String, String> pathAliases, String pname)
{
String[] entries = pname.split(fileSep);
// for now, we replace all found aliases with their source folders; if needed, we can
// compute all power sets made from pathAliases (will be 2^(pathAliases.size()) subsets)
StringBuilder sb = new StringBuilder();
boolean changed = false;
for (int i = 0; i < entries.length - 1; i++)
{
String entry = entries[i];
// if this entry is an alias, get its source
String source = pathAliases.get(entry);
changed = changed || source != null;
sb.append(source == null ? entry : source).append(fileSep);
}
sb.append(entries[entries.length - 1]);
return !changed ? new String[0] : new String[] { sb.toString() };
}
/**
* Get the legacy 4gL name for the Java class name.
*
* @param javaClassName
* The external-program's converted java name.
*
* @return The legacy program name or <code>null</code> if the given java
* program is not registered in name_map.xml.
*/
public static String getLegacySourceName(String javaClassName)
{
ExternalProgram extp = j2pMap.get(removePkg(javaClassName));
return extp == null ? null : extp.pname;
}
/**
* Get the legacy 4gL class name for the Java class name.
*
* @param javaClassName
* The converted FWD class mapping a legacy 4GL class name.
*
* @return The legacy program name or <code>null</code> if the given java
* program is not registered in name_map.xml.
*/
public static String getLegacyClassName(final String javaClassName)
{
ExternalProgram extp = j2pMap.get(removePkg(javaClassName));
return extp == null ? null : extp.ooname;
}
/**
* Get the legacy 4gL name for the Java class name or, if not a legacy program, return the
* java class name, without the base package.
*
* @param jname
* A java class name which may or may not be associated with a legacy 4GL program.
*
* @return The legacy program name or the java class name, without the base package.
*/
public static String convertJavaProgSafe(String jname)
{
String pname = getLegacySourceName(jname);
if (pname == null)
{
pname = removePkg(jname);
}
return pname;
}
/**
* Get the legacy 4GL name for the Java class name.
*
* @param javaClassName
* The external-program's converted java name.
* @param customRoot
* Custom root package name.
*
* @return The legacy program name or <code>null</code> if the given java
* program is not registered in name_map.xml.
*/
public static String getLegacySourceName(String customRoot, String javaClassName)
{
custpkgroot = customRoot;
initialize();
return getLegacySourceName(javaClassName);
}
/**
* Convert a java method name to a legacy name.
*
* @param clsName
* The class name where the method is defined.
* @param methodName
* The method's name.
* @param function
* Flag indicating that this method maps a legacy function (when {@code true)} or
* procedure (when {@code false}).
*
* @return The legacy method name or {@code null} if not found.
*/
public static String convertJavaMethod(String clsName, String methodName, boolean function)
{
ExternalProgram extp = j2pMap.get(removePkg(clsName));
if (extp != null)
{
Map<String, InternalEntry> ieMap = (function ? extp.j2pf : extp.j2pp);
InternalEntry ie = ieMap.get(methodName);
return (ie == null ? null : ie.pname);
}
// if not found
return null;
}
/**
* Normalize the given legacy external program name, considering the {@link WorkArea#caseSens} state.
*
* @param legacy
* The legacy name to be normalized
*
* @return The normalized legacy name.
*/
public static String normalizeLegacyName(String legacy)
{
if (legacy != null && !getLegacyCaseSensitive())
{
legacy = legacy.toLowerCase();
}
return legacy;
}
/**
* Uses the {@link #convertName(String, WorkArea, String[])} worker for finding the name of the Java class name of
* a converted P4GL procedure name.
*
* @param legacy
* Progress file name.
*
* @param searchPaths
* Paths where to search for the file instead of searching in the entire PROPATH.
*
* @return the Java class name for the given Progress file name if it is identified as a
* converted procedure from legacy sources. If no appropriate mapping is present,
* {@code null} is returned.
*/
public static String convertNameToClass(String legacy, String[] searchPaths)
{
WorkArea wa = local.get();
if (wa.isWindows)
{
legacy = dblBackslashRegexp.matcher(legacy).replaceAll(fileSep);
}
if (wa.caseSens == null)
{
wa.caseSens = getLegacyCaseSensitive();
}
String[] paths = convertName(legacy, wa, searchPaths);
return (paths != null) ? paths[0] : null;
}
/**
* Uses the {@link #fastConvertName(String)} worker for finding the name of the Java class name of
* a converted P4GL procedure name.
*
* @param legacy
* Progress file name.
* @return the Java class name for the given Progress file name if it is identified as a
* converted procedure from legacy sources. If no appropriate mapping is present,
* {@code null} is returned.
*/
public static String convertNameToClass(String legacy)
{
return convertNameToClass(legacy, true);
}
/**
* Uses the {@link #fastConvertName(String)} or {@link #convertName(String, WorkArea, String[])}
* worker for finding the name of the Java class name of a converted P4GL procedure name.
*
* @param legacy
* Progress file name.
* @param fastConvert
* True if we should use the {@link #fastConvertName(String)} worker.
* False if we should use the {@link #convertName(String, WorkArea, String[])} worker.
* @return the Java class name for the given Progress file name if it is identified as a
* converted procedure from legacy sources. If no appropriate mapping is present,
* {@code null} is returned.
*/
public static String convertNameToClass(String legacy, boolean fastConvert)
{
if (fastConvert)
{
String[] paths = fastConvertName(legacy);
return (paths != null) ? paths[0] : null;
}
else
{
return convertNameToClass(legacy, null);
}
}
/**
* Convert a legacy 4GL class name to a converted Java class file.
*
* @param legacyClsName
* The qualified legacy class name.
*
* @return the Java class name for the given legacy class name if it is identified as a
* converted procedure from legacy sources. If no appropriate mapping is present,
* {@code null} is returned.
*/
public static String convertOOToClass(String legacyClsName)
{
String lname = legacyClsName;
lname = lname.replace('.', '/');
lname = lname + ".cls";
String[] paths = fastConvertName(lname);
if (paths == null || paths[2] == null)
{
return null;
}
boolean caseSens = getLegacyCaseSensitive();
if ((caseSens && legacyClsName.equals(paths[2])) ||
(!caseSens && legacyClsName.equalsIgnoreCase(paths[2])))
{
// if the found program file defines the expected class name, then return its java name
return paths[0];
}
return null;
}
/**
* Uses the {@link #fastConvertName(String)} worker for detecting the if the {@code legacy} is a
* name of a converted legacy procedure. In affirmative case, the normalized name of the
* procedure filename is returned.
*
* @param legacy
* Progress file name.
*
* @return The legacy procedure name. If no appropriate mapping is present {@code null} is
* returned.
*/
public static String lookupLegacyName(String legacy)
{
String[] paths = fastConvertName(legacy);
return (paths != null) ? paths[1] : null;
}
/**
* Get the {@link #pkgroot package root}.
*
* @return The package root.
*/
public static String getPackageRoot()
{
return pkgroot;
}
/**
* Get the java name of the passed internal-entry and 4GL program.
*
* @param pname
* Legacy 4GL program name.
* @param ieName
* Legacy internal-entry name.
* @param function
* <code>true</code> if the search is for a user-defined
* function.
*
* @return The Java name of the given internal-entry, if this entry
* exists in the given program. Else, <code>null</code>
*/
public static String getMethodName(String pname, String ieName, boolean function)
{
ExternalProgram extProg = getExternalProgram(pname);
if (extProg == null)
{
return null;
}
InternalEntry method = extProg.searchp(ieName, function);
if (method == null)
{
return null;
}
return method.jname;
}
/**
* Get the map-to attribute of the passed internal-entry. The internal entry must be a
* function.
*
* @param pname
* Legacy 4GL program name.
* @param ieName
* Legacy internal-entry name.
*
* @return The map-to attribute of the passed internal-entry, if is a function.
* <code>null</code> if the {@link #ATTR_MAP_TO} does not exist or the internal
* entry does not resolve to a function.
*/
public static String getMapTo(String pname, String ieName)
{
ExternalProgram extProg = getExternalProgram(pname);
if (extProg == null)
{
return null;
}
InternalEntry ie = extProg.search(ieName, true);
if (ie == null)
{
return null;
}
return ie.getAttribute(ATTR_MAP_TO);
}
/**
* Get the library name attribute of the given internal-entry.
*
* @param pname
* Legacy 4GL program name.
* @param iename
* Legacy internal-entry name.
*
* @return The library name attribute of the given internal-entry name,
* assuming the program exists and the internal-entry is a native
* procedure definition. Else, <code>null</code>.
*/
public static String getLibraryName(String pname, String iename)
{
InternalEntry ie = getInternalEntry(pname, iename, false);
if (ie == null)
{
return null;
}
String type = ie.getAttribute(ATTR_TYPE);
if (InternalEntry.Type.DLL_ENTRY.toString().equalsIgnoreCase(type))
{
return ie.getAttribute(ATTR_LIBNAME);
}
return null;
}
/**
* Get the string representation of the parameter modes for this external
* procedure. If there are no parameters, return null.
*
* @param pname
* Legacy 4GL program name.
*
* @return See above.
*/
public static String getParameterModes(String pname)
{
return getParameterModes(pname, null, false);
}
/**
* Get the string representation of the parameter modes for this internal
* entry. If there are no parameters, return null.
*
* @param pname
* Legacy 4GL program name.
* @param ieName
* Legacy internal-entry name. If null or the empty string, the
* external procedure's parameters are used.
* @param function
* <code>true</code> if this is a function call.
*
* @return See above.
*/
public static String getParameterModes(String pname, String ieName, boolean function)
{
return getParameterModes(pname, null, ieName, function);
}
/**
* Get the string representation of the parameter modes for this internal
* entry. If there are no parameters, return null.
*
* @param pname
* Legacy 4GL program name.
* @param ie
* If non-null, the already resolved internal entry.
* @param ieName
* Legacy internal-entry name. If null or the empty string, the
* external procedure's parameters are used.
* @param function
* <code>true</code> if this is a function call.
*
* @return See above.
*/
public static String getParameterModes(String pname,
InternalEntry ie,
String ieName,
boolean function)
{
if (ie != null)
{
return ie.getParameterModes();
}
ExternalProgram extProg = getExternalProgram(pname);
if (extProg == null)
{
return null;
}
if (ieName == null || "".equals(ieName))
{
return extProg.main.getParameterModes();
}
ie = extProg.search(ieName, function);
return ie == null ? null : ie.getParameterModes();
}
/**
* Get the parameters for this internal entry. If there are no parameters, return an empty list.
* If the internal entry can't be found, return null.
*
* @param pname
* Legacy 4GL program name.
* @param ie
* If non-null, the already resolved internal entry.
* @param ieName
* Legacy internal-entry name. If null or the empty string, the
* external procedure's parameters are used.
* @param function
* <code>true</code> if this is a function call.
*
* @return See above.
*/
public static List<Parameter> getParameters(String pname,
InternalEntry ie,
String ieName,
boolean function)
{
if (ie != null)
{
return ie.getParameterList();
}
ExternalProgram extProg = getExternalProgram(pname);
if (extProg == null)
{
return null;
}
if (ieName == null || "".equals(ieName))
{
return extProg.main.getParameterList();
}
ie = extProg.search(ieName, function);
return ie == null ? null : ie.getParameterList();
}
/**
* Get the 4GL-style signature of the given internal-entry. If the program
* does not exist or the internal-entry does not belong to this program,
* return <code>null</code>.
*
* @param pname
* Legacy 4GL program name.
* @param ieName
* Legacy internal-entry name.
*
* @return See above.
*/
public static String getSignature(String pname, String ieName)
{
ExternalProgram extProg = getExternalProgram(pname);
if (extProg == null)
{
return "";
}
if (ieName == null || "".equals(ieName))
{
return extProg.main.getSignature();
}
InternalEntry ief = extProg.search(ieName, true);
// if both func and proc are with the same name, then func has priority
InternalEntry ie = ief != null ? ief : extProg.search(ieName, false);
return ie == null ? "" : ie.getSignature();
}
/**
* Get the internal-entries attribute of the given external procedure.
* If the program does not exist, return <code>null</code>.
*
* @param pname
* Legacy 4GL program name.
* @param prv
* Flag to include or not the private members.
*
* @return See above.
*/
public static String getInternalEntries(String pname, boolean prv)
{
ExternalProgram extProg = getExternalProgram(pname);
return (extProg == null) ? null : extProg.getInternalEntries(prv);
}
/**
* Get the published named events by the specified procedure.
*
* @param pname
* Legacy 4GL program name.
*
* @return The comma-separated list of published named events, when the event name is
* hard-coded.
*/
public static String getPublishedEvents(String pname)
{
ExternalProgram extProg = getExternalProgram(pname);
return (extProg == null) ? null : extProg.publishedEvents;
}
/**
* Get the extent of the parameter at 0-based position (for the given internal entry).
* If the parameter is dynamic-extent, then return 0, not -1.
*
* @param pname
* Legacy 4GL program name.
* @param ie
* If non-null, the already resolved internal entry.
* @param iename
* Legacy internal-entry name.
* @param func
* Flag indicating if this is a function or a procedure.
* @param param
* 0-based index of the parameter to be checked.
*
* @return See above.
*/
public static int getExtentForParam(String pname,
InternalEntry ie,
String iename,
boolean func,
int param)
{
if (ie != null)
{
return ie.getParameter(param).getExtent();
}
Parameter p = getParameter(pname, iename, func, param);
return (p == null) ? -1 : p.getExtent();
}
/**
* Get the type/class of the parameter at 0-based position (for the given internal entry).
*
* @param pname
* Legacy 4GL program name.
* @param ie
* If non-null, the already resolved internal entry.
* @param iename
* Legacy internal-entry name.
* @param func
* Flag indicating if this is a function or a procedure.
* @param param
* 0-based index of the parameter to be checked.
*
* @return the class of the requested parameter or {@code null} if parameters do not point
* to a valid parameter.
*/
public static Class getTypeForParam(String pname,
InternalEntry ie,
String iename,
boolean func,
int param)
{
if (ie != null)
{
return BaseDataType.fromTypeName(ie.getParameter(param).getType());
}
Parameter p = getParameter(pname, iename, func, param);
if (p == null)
{
return null;
}
return BaseDataType.fromTypeName(p.getType());
}
/**
* Check if the specified internal-entry is defined IN SUPER. If the program
* does not exist or the internal entry does not belong to the program,
* return <code>false</code>.
*
* @param pname
* Legacy 4GL program name.
* @param iename
* Legacy internal-entry name.
* @param function
* Flag indicating if this is a function or a procedure.
*
* @return See above.
*/
public static boolean isSuper(String pname, String iename, boolean function)
{
InternalEntry ie = getInternalEntry(pname, iename, function);
return (ie == null) ? false : ie.isSuper();
}
/**
* Check if the specified internal-entry is a function. If the program
* does not exist or the internal entry does not belong to the program,
* return <code>false</code>.
*
* @param pname
* Legacy 4GL program name.
* @param iename
* Legacy internal-entry name.
*
* @return See above.
*/
public static boolean isFunction(String pname, String iename)
{
return getInternalEntry(pname, iename, true) != null;
}
/**
* Check if the specified internal-entry is defined PRIVATE. If the program
* does not exist or the internal entry does not belong to the program,
* return <code>false</code>.
*
* @param pname
* Legacy 4GL program name.
* @param iename
* Legacy internal-entry name.
* @param function
* Flag indicating if this is a function or a procedure.
*
* @return See above.
*/
public static boolean isPrivate(String pname, String iename, boolean function)
{
InternalEntry ie = getInternalEntry(pname, iename, function);
return (ie == null) ? false : ie.isPrivate();
}
/**
* Check if the specified internal-entry is defined IN handle. If the
* program does not exist or the internal entry does not belong to the
* program, return <code>false</code>.
*
* @param pname
* Legacy 4GL program name.
* @param iename
* @param ie
* If non-null, the already resolved internal entry.
* Legacy internal-entry name.
* @param function
* Flag indicating if this is a function or a procedure.
*
* @return See above.
*/
public static boolean isInHandle(String pname,
InternalEntry ie,
String iename,
boolean function)
{
if (ie != null)
{
return ie.isInHandle();
}
ie = getInternalEntry(pname, iename, function);
return (ie == null) ? false : ie.isInHandle();
}
/**
* Helper function to obtain the Java object that holds the state of a single internal entry.
*
* @param pname
* Legacy 4GL program name (the external procedure).
* @param iename
* Legacy internal-entry name.
* @param func
* <code>true</code> if the search should be for a function.
*
* @return The internal entry or <code>null</code> if the external procedure does not exist
* or if the external procedure has no such internal entry.
*/
public static InternalEntry getInternalEntry(String pname, String iename, boolean func)
{
ExternalProgram extProg = getExternalProgram(pname);
if (extProg == null)
{
return null;
}
return extProg.search(iename, func);
}
/**
* Get the list of defined constructors for the legacy type.
*
* @param type
* The legacy class.
*/
public static List<InternalEntry> getConstructors(Class<? extends _BaseObject_> type)
{
registerLegacyClass(type);
LegacyClass lcls = classMap.get(type);
return new LinkedList<>(lcls.ctors);
}
/**
* Get the static constructor for the legacy type.
*
* @param type
* The legacy class.
*/
public static InternalEntry getStaticConstructor(Class<? extends _BaseObject_> type)
{
registerLegacyClass(type);
LegacyClass lcls = classMap.get(type);
return lcls.staticCtor;
}
/**
* Get {@link LegacyClass#executeMethods execute method names} names for this legacy class and its hierarchy.
*
* @param type
* The legacy class for which the method name must be resolved.
*
* @return The method names, under the form '__qualified_converted_name_execute__'.
*/
public static Method[] getExecuteMethods(Class<? extends _BaseObject_> type)
{
registerLegacyClass(type);
LegacyClass lcls = classMap.get(type);
return lcls.executeMethods;
}
/**
* Get the destructor for the legacy type.
*
* @param type
* The legacy class.
*
* @return The list of destructors for this class (and all super-classes), with the order from the
* super-class parents to this class.
*/
public static List<InternalEntry> getDestructors(Class<? extends _BaseObject_> type)
{
registerLegacyClass(type);
LegacyClass lcls = classMap.get(type);
return lcls.dtors;
}
/**
* Get all the overloaded methods in the given legacy class, with the specified name,
* and with name with possible _[0-9]+ suffix.
*
* @param type
* The legacy {@link com.goldencode.p2j.oo.lang._BaseObject_} sub-class.
* @param iename
* The legacy name.
* @return list of the overloaded methods.
*/
public static List<InternalEntry> getOverloads(Class<?> type, String iename)
{
registerLegacyClass(type);
String name = iename.toString().toLowerCase().intern();
int len = name.length();
Map<String, List<InternalEntry>> methods = classMap.get(type).overloads;
List<InternalEntry> overloads = new LinkedList<>();
if (methods.containsKey(name))
{
overloads.addAll(methods.get(name));
}
methods.entrySet().stream().
filter(e -> e.getKey().startsWith(name) &&
OVERLOAD_SUFFIX.matcher(e.getKey().substring(len)).matches()).
map(Map.Entry::getValue).forEach(overloads::addAll);
// List<InternalEntry> overloads = methods.get(name);
return overloads.isEmpty() ? null : overloads;
}
/**
* Get all overloads of this method name; only the overrides of the methods are included (their super-class
* definitions matching name and parameters will not be included).
*
* @param type
* The legacy {@link com.goldencode.p2j.oo.lang._BaseObject_} sub-class.
* @param iename
* The legacy name.
*
* @return list of the overloaded methods, from all super-classes.
*/
public static List<InternalEntry> getOverrides(Class<?> type, String iename)
{
registerLegacyClass(type);
LegacyClass lc = classMap.get(type);
return new ArrayList<>(lc.getOverrides(type, iename));
}
/**
* Get all GET functions of a given class; only the getters for the class are included (any super-class GET
* function will not be included).
*
* @param type
* The legacy {@link com.goldencode.p2j.oo.lang._BaseObject_} sub-class.
* @return list of the GET methods from the _BaseObject_ sub-class.
*/
public static Map<String, List<InternalEntry>> getGetters(Class<? extends _BaseObject_> type)
{
registerLegacyClass(type);
LegacyClass lcls = classMap.get(type);
return new HashMap<>(lcls.getters);
}
/**
* Get all SET functions of a given class; only the getters for the class are included (any super-class SET
* function will not be included).
*
* @param type
* The legacy {@link com.goldencode.p2j.oo.lang._BaseObject_} sub-class.
* @return list of the SET methods from the _BaseObject_ sub-class.
*/
public static Map<String, List<InternalEntry>> getSetters(Class<? extends _BaseObject_> type)
{
registerLegacyClass(type);
LegacyClass lcls = classMap.get(type);
return new HashMap<>(lcls.setters);
}
/**
* Register the specified legacy class.
* <p>
* This is used to register legacy builtin 4GL classes - these always need to be marked with
* the {@link LegacyResource} annotation.
*
* @param type
* The class representing a builtin 4GL type.
*/
public static void registerLegacyClass(Class<?> type)
{
if (!classMap.containsKey(type))
{
synchronized (SourceNameMapper.class)
{
LegacyClass lc = classMap.get(type);
if (lc != null)
{
return;
}
String pname = null;
if (type.getPackage().getName().startsWith("com.goldencode.p2j.oo"))
{
// must be a builtin legacy class
LegacyResource legacyResource = type.getAnnotation(LegacyResource.class);
if (legacyResource == null)
{
if (type == _BaseObject_.class)
{
pname = "Progress.Lang.Object";
}
else
{
throw new RuntimeException("No @LegacyResource annotation for " + type.getName());
}
}
else
{
pname = legacyResource.resource();
}
}
else
{
String jname = type.getName();
jname = jname.substring(getPackageRoot().length() + 1);
ExternalProgram extProg = j2pMap.get(jname);
if (extProg != null)
{
// this is an application class
pname = extProg.ooname;
}
}
if (pname == null)
{
throw new RuntimeException("Type " + type + " is not associated with a legacy class!");
}
LegacyClass lcls = buildLegacyClass(type);
SourceNameMapper.classMap.put(type, lcls);
}
}
}
/**
* Find the property's extent, if it applies.
*
* @param type
* The legacy OO class.
* @param pname
* The legacy OO property name.
*
* @return The property's extent, or <code>null</code> if it does not apply.
*/
public static Integer findLegacyPropertyExtent(Class<?> type, String pname)
{
registerLegacyClass(type);
LegacyClass cls = classMap.get(type);
Integer extent = cls.extents.get(pname);
if (extent != null)
{
return extent;
}
if (type.isInterface())
{
return null;
}
type = type.getSuperclass();
return _BaseObject_.class.isAssignableFrom(type) ? findLegacyPropertyExtent(type, pname) : null;
}
/**
* Look into all the properties with the specified legacy name (in the given legacy type) and
* return the getter or setter matching exactly the specified signature.
*
* @param type
* The class matching the legacy OO type.
* @param pname
* The legacy property name.
* @param getter
* Flag indicating to retrieve a getter or setter.
* @param isExtent
* Flag indicating whether the property is defined as extent, meaning
* we have to retrieve the setter method we need.
* @param indexed
* Flag indicating to retrieve the indexed getter or setter.
*
* @return The found {@link InternalEntry} or <code>null</code> if not found.
*/
public static InternalEntry findLegacyAccessor(Class<?> type,
String pname,
boolean getter,
boolean isExtent,
boolean indexed)
{
registerLegacyClass(type);
int targetSize = (getter ? 0 : 1) + (indexed ? 1 : 0);
LegacyClass cls = classMap.get(type);
List<InternalEntry> methods = (getter ? cls.getters : cls.setters).get(pname.toLowerCase());
if (methods != null)
{
if (!indexed && methods.size() == 1)
{
return methods.get(0);
}
if (isExtent && !getter)
{
for (InternalEntry candidate : methods)
{
// match on the method receiving one extent argument
if (candidate.getParameterList().size() == 1 &&
candidate.getParameter(0).isExtent())
{
return candidate;
}
}
}
else
{
for (InternalEntry candidate : methods)
{
// we match only the signature, not the type (METHOD/EVENT) or the name
if (candidate.getParameterListSize() != targetSize)
{
continue;
}
// found it
return candidate;
}
}
}
if (type.isInterface())
{
return null;
}
type = type.getSuperclass();
return _BaseObject_.class.isAssignableFrom(type)
? findLegacyAccessor(type, pname, getter, isExtent, indexed)
: null;
}
/**
* Returns a list of all the setters associated with this property. Allows for choosing the
* right one at run-time. Emitted for DYNAMIC-INVOKE API.
*
* @param type
* The class matching the legacy OO type.
* @param propName
* The legacy property name.
*
* @return The list containing all {@link InternalEntry}'s representing the different setter
* methods.
*/
public static List<InternalEntry> findLegacySetters(Class<?> type, String propName)
{
registerLegacyClass(type);
LegacyClass cls = classMap.get(type);
List<InternalEntry> methods = cls.setters.get(propName.toLowerCase());
return methods;
}
/**
* Check if the given legacy class defines a method with a given name.
*
* @param pname
* The legacy method name.
*
* @return <code>true</code> if there is a method defined with the given name.
*/
public static boolean hasLegacyMethod(Class<?> type, String pname)
{
registerLegacyClass(type);
List<InternalEntry> methods = classMap.get(type).overloads.get(pname.toLowerCase());
if (methods != null)
{
for (InternalEntry candidate : methods)
{
if (candidate.getLegacyName().equalsIgnoreCase(pname))
{
return true;
}
}
}
type = type.getSuperclass();
return _BaseObject_.class.isAssignableFrom(type) ? hasLegacyMethod(type, pname)
: false;
}
/**
* Given a converted class name, find the legacy name for a Java method defined in that program or legacy
* class.
*
* @param type
* The converted class name.
* @param mname
* The Java method name.
*
* @return See above.
*/
public static String findLegacyEntryName(Class<?> type, String mname)
{
registerLegacyClass(type);
LegacyClass lc = classMap.get(type);
Function<Collection<List<InternalEntry>>, String> searchName = (l) ->
{
for (List<InternalEntry> l2 : l)
{
for (InternalEntry ie : l2)
{
if (ie.jname.equals(mname))
{
return ie.pname;
}
}
}
return null;
};
String iename = searchName.apply(lc.overloads.values());
if (iename == null)
{
iename = searchName.apply(lc.setters.values());
}
if (iename == null)
{
iename = searchName.apply(lc.getters.values());
}
if (iename == null)
{
Function<List<InternalEntry>, String> searchName2 = (l) ->
{
for (InternalEntry ie : l)
{
if (ie.jname.equals(mname))
{
return ie.pname;
}
}
return null;
};
iename = searchName2.apply(lc.ctors);
if (iename == null && lc.dtor != null && lc.dtor.jname.equals(mname))
{
iename = lc.dtor.pname;
}
if (iename == null && lc.staticCtor != null && lc.staticCtor.jname.equals(mname))
{
iename = lc.staticCtor.pname;
}
}
return iename;
}
/**
* Look into all the methods with the specified legacy name (in the given type) and return the
* one matching exactly the specified signature.
*
* @param type
* The class matching the legacy OO type.
* @param pname
* The legacy method name.
* @param signature
* The legacy signature to match.
*/
public static InternalEntry findLegacyMethod(Class<?> type,
String pname,
InternalEntry signature)
{
registerLegacyClass(type);
List<InternalEntry> methods = classMap.get(type).overloads.get(pname.toLowerCase());
if (methods != null)
{
l1: for (InternalEntry candidate : methods)
{
// we match only the signature, not the type (METHOD/EVENT) or the name
if (candidate.getParameterListSize() != signature.getParameterListSize() ||
!Objects.equals(candidate.getParameterModes(), signature.getParameterModes()) ||
!Objects.equals(candidate.getExtent(), signature.getExtent()) ||
!Objects.equals(candidate.getQualified(), signature.getQualified()))
{
continue;
}
for (int i = 0; i < candidate.getParameterListSize(); i++)
{
Parameter pcand = candidate.getParameter(i);
Parameter psign = signature.getParameter(i);
if (pcand.getExtent() != psign.getExtent() ||
!pcand.getType().equals(psign.getType()) ||
!pcand.getMode().equals(psign.getMode()))
{
continue l1;
}
// check the qualified OO name
if (pcand.getQualified() != null && psign.getQualified() != null)
{
Class<?> clsCand = ObjectOps.resolveClass(pcand.getQualified());
Class<?> clsSign = ObjectOps.resolveClass(psign.getQualified());
String mode = pcand.getMode();
if ("INPUT".equals(mode))
{
if (!clsCand.isAssignableFrom(clsSign))
{
continue l1;
}
}
else if ("OUTPUT".equals(mode))
{
if (!clsSign.isAssignableFrom(clsCand))
{
continue l1;
}
}
else if ("INPUT-OUTPUT".equals(mode))
{
if (clsCand != clsSign)
{
continue l1;
}
}
}
else if (!(pcand.getQualified() == null && psign.getQualified() == null))
{
continue l1;
}
}
// all equals, return it
return candidate;
}
}
type = type.getSuperclass();
return _BaseObject_.class.isAssignableFrom(type) ? findLegacyMethod(type, pname, signature)
: null;
}
/**
* Remove any <code>..</code> entries from the given relative path name.
*
* @param relName
* Relative file path.
*
* @return The resolved name.
*/
public static String removeUpPath(String relName)
{
FastStringBuilder bufRelative = new FastStringBuilder(relName.length());
String[] lstRelative = relName.split(fileSep);
int eat = 0;
boolean inContent = false;
for (int i = lstRelative.length - 1; i >= 0; i--)
{
String token = lstRelative[i];
if (token.equals(".."))
{
eat++;
continue;
}
if (eat > 0)
{
eat--;
continue;
}
if (eat == 0 && inContent)
{
bufRelative.insertLeft(fileSepChar);
}
inContent = true;
bufRelative.insertLeft(token);
}
// New absolute name based on relative one but without extra ".."
return bufRelative.toString();
}
/**
* Convert the given Progress file name into a simple relative path
* form. Any leading file separator is removed. All references
* to directories <code>.</code> and <code>..</code> are
* processed. Empty path elements are removed.
* <p>
* If {@link WorkArea#caseSens} is false, the program name is lower-cased.
*
* @param progName
* Input name.
* @param caseSens
* Case sensitivity of the passed in input name.
*
* @return The relative name.
*/
public static String canonicalize(String progName, boolean caseSens)
{
// honor case-insensitivity
if (!caseSens)
{
progName = progName.toLowerCase();
}
FastStringBuilder bufRelative = new FastStringBuilder(progName.length());
String[] pathTokens = progName.split(fileSep);
int eat = 0;
boolean inContent = false;
for (int i = pathTokens.length - 1; i >= 0; i--)
{
String token = pathTokens[i];
if (token.equals("") && i > 0 || token.equals("."))
{
continue;
}
else if (token.equals(".."))
{
eat++;
continue;
}
if (eat > 0)
{
eat--;
continue;
}
if (eat == 0 && inContent)
{
bufRelative.insertLeft(fileSepChar);
}
inContent = true;
bufRelative.insertLeft(token);
}
// New absolute name based on relative one but without extra ".."
return bufRelative.toString();
}
/**
* Resolve the execute method names for this legacy class and its hierarchy.
*
* @param cls
* The legacy class for which the method name must be resolved.
*
* @return The method names, under the form '__qualified_converted_name_execute__'.
*/
private static Method[] resolveExecuteMethods(Class<?> cls)
{
// enums are not tracked and have no execute methods.
// interfaces have no execute methods.
if (LegacyEnum.class.isAssignableFrom(cls) || cls.isInterface())
{
return new Method[0];
}
ArrayList<Method> methods = new ArrayList<>();
while (cls != Object.class)
{
Method m = resolveExecuteMethod(cls);
if (m != null)
{
methods.add(m);
}
cls = cls.getSuperclass();
}
return methods.toArray(new Method[0]);
}
/**
* Resolve the name of this legacy class converted 'execute' method.
*
* @param cls
* The legacy class for which the method name must be resolved.
*
* @return The method name, under the form '__qualified_converted_name_execute__'.
*/
private static Method resolveExecuteMethod(Class<?> cls)
{
// enums are not tracked and have no execute method
if (LegacyEnum.class.isAssignableFrom(cls))
{
return null;
}
// go through all declared methods and get the one with Type.EXECUTE annotation
Method[] mthds = cls.getDeclaredMethods();
for (int i = 0; i < mthds.length; i++)
{
Method mthd = mthds[i];
LegacySignature ls = mthd.getAnnotation(LegacySignature.class);
if (ls != null && ls.type() == Type.EXECUTE)
{
return mthd;
}
}
return null;
}
/**
* Creates a new SourceNameMapperCache for the given propath value using an
* AssociatedThread in order to access the current context.
* If the initial thread that reads the path-mapping nodes from <code>name_map.xml</code>
* did not complete, the method is short-circuited.
*
* @param propath
* The propath for which the cache will be created.
* @param propathHash
* The hash code of the propath.
*/
private static void createSourceCacheEntry(String[] propath, Long propathHash)
{
if (!isPathMappingInitialized())
{
return;
}
if (MonitorRunnable.hasIdentifier(propathHash))
{
LOG.finer("Previously submitted task identifier was found, skipping the task.");
return;
}
Runnable task = new Runnable()
{
@Override
public void run()
{
// check again if there is no other thread doing work (the identifier is pushed when the thread is
// created not when this 'createSourceCacheEntry' starts, so that check is too early).
if (MonitorRunnable.hasIdentifier(propathHash))
{
LOG.finer("Previously submitted task identifier was found, skipping the task.");
return;
}
// Create a map that will use the integer value from the initializationPaths
// as the key and the result from fastConvertName as the value. This map
// is stored in the SourceNameMapperCache and reduces String usage by only
// having a single map of paths instead of mapping the path to the result
// obtained.
LRUCache<Integer, String[]> resolvedNames = CacheManager.createLRUCache(SourceNameMapper.class,
"names",
16384,
null,
CapacityPolicy.LENIENT);
// TODO: this can terminate when the cache is full, no reason to go through all the paths to
// just evict them from cache
for (Map.Entry<String, Integer> entry : initializationPaths.entrySet())
{
String path = entry.getKey();
Integer index = entry.getValue();
String[] res = fastConvertName(path, true);
resolvedNames.put(index, res);
}
SourceNameMapperCache mapperCache = new SourceNameMapperCache(propath, resolvedNames);
synchronized (sourceCache)
{
// do not overwrite if some other thread has already computed the cache for this hash
sourceCache.putIfAbsent(propathHash, mapperCache);
}
MonitorRunnable.removeIdentifier(propathHash);
}
};
MonitorRunnable namedTask = new MonitorRunnable(propathHash, task);
mappingExecutor.execute(namedTask);
}
/**
* Writes an entry for the program name given into the propath cache, but only if one is
* available. New values are added to {@link #initializationPaths} when writing the cache
* so that other caches that will be created to directly use the available resource instead
* of separately caching the value when searching. When a new value is added to the
* {@link #initializationPaths} map, it will not add the result of the search to other
* existent caches, that's why it is necessary to check if the cache doesn't have the
* path mapped instead of relying on the already available map.
*
* @param propath
* The propath used to find the cache in which the new entry will be added.
* @param propathHash
* The hash code of the propath.
* @param programName
* The key for the map in the propath cache.
* @param convertResult
* The value.
*/
private static void writeConvertedCache(String[] propath,
Long propathHash,
String programName,
String[] convertResult)
{
if (!isPathMappingInitialized())
{
return;
}
SourceNameMapperCache convertedCache;
synchronized (sourceCache)
{
convertedCache = sourceCache.get(propathHash);
}
if (convertedCache == null)
{
return;
}
Integer mappingIndex = initializationPaths.get(programName);
convertedCache.readLock();
try
{
// Initialization paths added by other caches will not be present in
// the other caches, since their results are not computed.
if (mappingIndex != null && convertedCache.hasProgram(mappingIndex))
{
return;
}
// check if propath matches before returning
String[] cachePropath = convertedCache.getPropath();
if(!Arrays.equals(propath, cachePropath))
{
return;
}
}
finally
{
convertedCache.readUnlock();
}
// A path that was added by another cache must use the existent value
// from the map and not increment the counter.
if (mappingIndex == null)
{
mappingIndex = MAPPING_COUNTER.getAndIncrement();
}
initializationPaths.put(programName, mappingIndex);
convertedCache.writeLock();
try
{
convertedCache.addResolvedProgram(mappingIndex, convertResult);
}
finally
{
convertedCache.writeUnlock();
}
}
/**
* Convert a Progress file name into Java class name. This is an initial search method
* which will try to short-circuit the whole search process of {@link #convertName(String, WorkArea)}
* by using {@link #pathLookups} map of lookup instances. The lookup map contains
* SinglePathLookup and MultiPathLookup instances.
* <p>
* SinglePathLookup looks up the program name based on the propath and the case sensitivity
* values of the WorkArea. If there are path aliases in use, then it will resolve the program
* name and redo the lookup process using each path alias available according to the propath
* and case sensitivity parameters.
* <p>
* MultiPathLookup stores a list of SinglePathLookup instances and will use their lookup method
* to search for the program name. Multiple matches for the same program name will cause a conflict
* and in this case, <code>null</code> will be returned.
* <p>
* If no match was found by SinglePathLookup/MultiPathLookup, or there is no entry for the
* unqualified program name in the lookup map, the search will fallback to the old implementation.
*
* @param legacyProgName
* Progress file name.
*
* @return A {@code String} array with three components:
* <ol>
* <li>Java class name for the given Progress file name as first element</li>
* <li>The legacy procedure name in second element of the array.</li>
* <li>The legacy qualified OO name (if the name represents a legacy class or
* interface).</li>
* </ol>
* If no appropriate mapping is present {@code null} is returned.
*/
private static String[] fastConvertName(String legacyProgName)
{
return fastConvertName(legacyProgName, false);
}
/**
* Convert a Progress file name into Java class name. This is an initial search method
* which will try to short-circuit the whole search process of {@link #convertName(String, WorkArea)}
* by using {@link #pathLookups} map of lookup instances. The lookup map contains
* SinglePathLookup and MultiPathLookup instances.
* <p>
* SinglePathLookup looks up the program name based on the propath and the case sensitivity
* values of the WorkArea. If there are path aliases in use, then it will resolve the program
* name and redo the lookup process using each path alias available according to the propath
* and case sensitivity parameters.
* <p>
* MultiPathLookup stores a list of SinglePathLookup instances and will use their lookup method
* to search for the program name. Multiple matches for the same program name will cause a conflict
* and in this case, <code>null</code> will be returned.
* <p>
* If no match was found by SinglePathLookup/MultiPathLookup, or there is no entry for the
* unqualified program name in the lookup map, the search will fallback to the old implementation.
*
* @param legacyProgName
* Progress file name.
* @param avoidCache
* <code>false</code> is the default value and allows reading from the cache or creating
* new entries. <code>true</code> is only used by the thread creating the source cache
* entry to avoid concurrent modification.
*
* @return A {@code String} array with three components:
* <ol>
* <li>Java class name for the given Progress file name as first element</li>
* <li>The legacy procedure name in second element of the array.</li>
* <li>The legacy qualified OO name (if the name represents a legacy class or
* interface).</li>
* </ol>
* If no appropriate mapping is present {@code null} is returned.
*/
private static String[] fastConvertName(String legacyProgName, boolean avoidCache)
{
// Lookup the extension of the file and check if it matches any available
// extension that was saved when the procedure maps were created. If the
// extension does not match any of the available ones from the set, it should
// not continue the search any further.
// Program names without an extension will pass this condition.
int lastDotIndex = legacyProgName.lastIndexOf(".");
if (lastDotIndex > 0)
{
String extension = legacyProgName.substring(lastDotIndex + 1);
if (!allowedLegacyExtensions.contains(extension))
{
return null;
}
}
WorkArea wa = local.get();
// get rid of the Windows file separator in working strings
if (wa.isWindows)
{
legacyProgName = dblBackslashRegexp.matcher(legacyProgName).replaceAll(fileSep);
}
// internally, we use the canonicalized paths (lowercased and / for path separator)
// but for the result, we use the original case and path separator (as 4GL does)
if (wa.propath == null)
{
// just let it know the propath needs to be resolved - as it must be null originally
String searchPath = EnvironmentOps.getSearchPath().toStringMessage();
setSearchPath(searchPath);
}
if (wa.caseSens == null)
{
wa.caseSens = EnvironmentOps.getLegacyCaseSensitive();
}
legacyProgName = canonicalize(legacyProgName, wa.caseSens);
boolean hasCache = isPathMappingInitialized() && !avoidCache;
if (hasCache)
{
SourceNameMapperCache convertedCache;
synchronized (sourceCache)
{
convertedCache = sourceCache.get(wa.propathHash);
}
if (convertedCache == null)
{
hasCache = false;
}
else
{
convertedCache.readLock();
try
{
// check if propath matches before returning
String[] cachePropath = convertedCache.getPropath();
if(!Arrays.equals(wa.propath, cachePropath))
{
hasCache = false;
}
}
finally
{
convertedCache.readUnlock();
}
if (hasCache)
{
Integer index = initializationPaths.get(legacyProgName);
convertedCache.readLock();
try
{
if (index != null && convertedCache.hasProgram(index))
{
return convertedCache.getResolvedProgram(index);
}
}
finally
{
convertedCache.readUnlock();
}
}
}
}
// create a new cache for the current propath
if (!hasCache && !avoidCache)
{
createSourceCacheEntry(wa.propath, wa.propathHash);
}
// lookup the unqualified program name in the map
int index = legacyProgName.lastIndexOf(fileSepChar);
String progName = index > 0 ? legacyProgName.substring(index + 1) : legacyProgName;
PathLookup lookup = pathLookups.get(progName);
boolean modified = false;
if (lookup == null)
{
// check if we have a r-code file and if so, make it a p-code file and check again
if (progName.endsWith(".r") || (!wa.caseSens && progName.endsWith(".R")))
{
String noExtProgName = progName.substring(0, progName.length() - ".r".length());
String pCodeName = noExtProgName + (wa.caseSens ? ".p" : ".P");
progName = pCodeName;
lookup = pathLookups.get(pCodeName);
if (lookup == null)
{
pCodeName = noExtProgName + (wa.caseSens ? ".w" : ".W");
progName = pCodeName;
lookup = pathLookups.get(pCodeName);
}
modified = lookup != null;
}
}
if (lookup != null)
{
String currentProgram;
if (modified)
{
// If we have a r-code file and it's p-code version was found in the pathLookups map,
// we have to lookup the p-code file instead of the current legacyProgName
String modifiedLegacyProgName = legacyProgName.substring(0, index + 1) + progName;
currentProgram = lookup.lookupPath(modifiedLegacyProgName, wa.propath, wa.caseSens);
}
else
{
currentProgram = lookup.lookupPath(legacyProgName, wa.propath, wa.caseSens);
}
if (currentProgram == null)
{
String[] result = convertName(legacyProgName, wa);
if (!avoidCache)
{
writeConvertedCache(wa.propath, wa.propathHash, legacyProgName, result);
}
return result;
}
ExternalProgram extProg = wa.caseSens ? p2jMap.get(currentProgram) : p2jMap_ci.get(currentProgram);
// Save the legacyProgName before switching slashes into backslashes, otherwise
// it will cache the wrong value and not be able to retrieve it since it's different.
String tmpLegacyName = legacyProgName;
if (wa.isWindows)
{
// 4GL automatically switch all slashes into backslashes on Win OS
legacyProgName = fileSepRegexp.matcher(legacyProgName).replaceAll("\\\\");
}
String[] result = new String[] { addPkg(extProg.jname), legacyProgName, extProg.ooname };
if (!avoidCache)
{
writeConvertedCache(wa.propath, wa.propathHash, tmpLegacyName, result);
}
return result;
}
String[] result = convertName(legacyProgName, wa);
if (!avoidCache)
{
writeConvertedCache(wa.propath, wa.propathHash, legacyProgName, result);
}
return result;
}
/**
* Convert a Progress file name into Java class name. The input name can be an absolute file
* name, a file name relative to current location or a file name relative to some directory
* in a given list of directories .
* Before processing, the input file name is processed by {@link #canonicalize}.
* <p>
* A simple lookup is tried first, taking into account if there is a .r, .p, or .w extension
* which may have been left off the 4GL statement. If this fails direct check for absolute path
* if attempted using one of the roots from {@code legacyRoots}. In multiple project scenario,
* the project name is also added as a path token in order to match the structure from
* {@code name_map.xml}.
* <p>
* If this mapping does not exist then each path segment of the list of directories is tried in order.
* The given program name is check to see if it is prefixed with the directory name, if so
* that prefix is removed and a lookup is done with this name. The first match found ends the
* search.
* <p>
* Absolute paths are converted into relative paths based on the existence of a matching path
* in the legacy PROPATH (see {@link EnvironmentOps#getLegacySearchPath}). To handle additional
* absolute paths, one must add the appropriate path prefix to the legacy PROPATH and it will
* be removed here.
* <p>
* This same PROPATH processing is how names relative to the current directory can be handled.
* This is the only way to handle that case since the current directory in the source system is
* runtime information that is not available here.
* <p>
* The legacy PROPATH is the key configuration tool by which one specifies to this method how
* to remove absolute and current directory relative prefixes. This means that the legacy
* PROPATH that is used here may need to be different than the PROPATH on the original system.
* <p>
* As a fallback, if no mapping can be found and if the given {@code progName} is a fully
* qualified Java class name which references a valid Java class, then the {@code progName} is
* returned unchanged.
* <p>
* In Windows it can verify the absolute or relative program name is in current PROPATH
* directory set. This allows to properly map the following program names used in
* PROGRESS RUN statement:
* "c:/path/to/project/stcases/uast/rundir/absolute/program.p"
* "rundir/../../uast/rundir/relative/test11.p."
* "../uast/rundir/relative/test5.p"
* <p>
* If no program name is explicitly found with a <code>.r</code> extension, R-code program
* names are looked up using the {@link #p2jNonExtMap} (the program name without the extension).
* <p>
* If {@link #pathAliases} is specified, and the program can be found (as it is or after
* removing the <code>.r</code> extension) using the directories list, the lookup falls back to resolving
* the aliased folders in the program name, via {@link #resolvePathAliases}.
*
*
* @param legacyProgName
* Progress file name.
* @param wa
* WorkArea of the current local context.
* @param searchPaths
* Paths to use when searching for the Progress file. If null, will search in propath.
*
* @return A {@code String} array with three components:
* <ol>
* <li>Java class name for the given Progress file name as first element</li>
* <li>The legacy procedure name in second element of the array.</li>
* <li>The legacy qualified OO name (if the name represents a legacy class or
* interface).</li>
* </ol>
* If no appropriate mapping is present {@code null} is returned.
*/
private static String[] convertName(String legacyProgName, WorkArea wa, String[] searchPaths)
{
Function<String, ExternalProgram> lookupDirectName;
Function<String, ExternalProgram> lookupCompiledRcode;
Function<String, ExternalProgram> lookupAliasedDirectName;
Function<String, ExternalProgram> lookupAliasedCompiledRcode;
boolean caseSens = wa.caseSens;
// TODO: RUN can target r-code/procedures even if the extension is missing; i.e. RUN foo. will execute
// foo.r or foo.p, if it exists. There may be PROPATH involvement, as well.
Function<String, Boolean> isRcode =
(pname -> pname.endsWith(".r") || (!caseSens && pname.endsWith(".R")));
boolean legacyRcode = isRcode.apply(legacyProgName);
Function<String, Boolean> isPcode =
(pname -> pname.endsWith(".p") || (!caseSens && pname.endsWith(".P")) ||
pname.endsWith(".w") || (!caseSens && pname.endsWith(".W")));
boolean legacyPcode = isPcode.apply(legacyProgName);
boolean noExt = !legacyPcode && !legacyRcode;
boolean hasAliases = pathAliases != null;
String msg = "Found first match %s for program %s, and additional %s";
lookupDirectName = (pname ->
{
// Check for direct name before looking for noExt name
ExternalProgram extProg = getExternalProgram(pname);
if (extProg == null)
{
pname = (noExt) ? pname : pname.substring(0, pname.length() - ".p".length());
// we could use a try, but keeping a match of names without extension to the full
// program names (with extension) is easier
List<ExternalProgram> progs = caseSens ? p2jNonExtMap.get(pname)
: p2jNonExtMap_ci.get(pname);
if (progs != null && !progs.isEmpty())
{
extProg = progs.get(0);
if (LOG.isLoggable(Level.WARNING))
{
for (int i = 1; i < progs.size(); i++)
{
LOG.log(Level.WARNING, msg, extProg.pname, legacyProgName, progs.get(i).pname);
}
}
}
}
return extProg;
});
lookupCompiledRcode = !legacyRcode ? null : (pname ->
{
if (!isRcode.apply(pname))
{
return null;
}
pname = pname.substring(0, pname.length() - ".r".length());
ExternalProgram extProg = null;
// we could use a trie, but keeping a match of names without extension to the full
// program names (with extension) is easier
List<ExternalProgram> progs = caseSens ? p2jNonExtMap.get(pname)
: p2jNonExtMap_ci.get(pname);
if (progs != null && !progs.isEmpty())
{
extProg = progs.get(0);
if (LOG.isLoggable(Level.WARNING))
{
for (int i = 1; i < progs.size(); i++)
{
LOG.log(Level.WARNING, String.format(msg,
extProg.pname,
legacyProgName,
progs.get(i).pname));
}
}
}
return extProg;
});
lookupAliasedDirectName = !hasAliases ? null : (pname ->
{
String[] aliases = resolvePathAliases(pname);
ExternalProgram extProg = null;
for (String aliasName : aliases)
{
ExternalProgram prog = lookupDirectName.apply(aliasName);
if (prog != null)
{
if (extProg != null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, String.format(msg,
extProg.pname,
legacyProgName,
prog.pname));
}
}
else
{
extProg = prog;
}
}
}
return extProg;
});
lookupAliasedCompiledRcode = !legacyRcode || !hasAliases ? null : (pname ->
{
if (!isRcode.apply(pname))
{
return null;
}
String[] aliases = resolvePathAliases(pname);
ExternalProgram extProg = null;
for (String aliasName : aliases)
{
ExternalProgram prog = lookupCompiledRcode.apply(aliasName);
if (prog != null)
{
if (extProg != null)
{
LOG.log(Level.WARNING, String.format(msg,
extProg.pname,
legacyProgName,
prog.pname));
}
else
{
extProg = prog;
}
}
}
return extProg;
});
@SuppressWarnings("unchecked")
Function<String, ExternalProgram>[] lookups = new Function[]
{
lookupDirectName,
lookupCompiledRcode,
lookupAliasedDirectName,
lookupAliasedCompiledRcode,
};
for (Function<String, ExternalProgram> lookup : lookups)
{
if (lookup == null)
{
continue;
}
String[] res = convertName(legacyProgName, lookup, wa, searchPaths);
if (res != null)
{
return res;
}
}
return null;
}
/**
* Uses {@link #convertName(String, WorkArea, String[])} with the PROPATH segments for the second parameter.
* Consult the javadoc for the overloaded method with 3 parameters for more information.
*
* @param legacyProgName
* Progress file name.
* @param wa
* WorkArea of the current local context.
*
* @return A {@code String} array with three components:
* <ol>
* <li>Java class name for the given Progress file name as first element</li>
* <li>The legacy procedure name in second element of the array.</li>
* <li>The legacy qualified OO name (if the name represents a legacy class or
* interface).</li>
* </ol>
* If no appropriate mapping is present {@code null} is returned.
*/
private static String[] convertName(String legacyProgName, WorkArea wa)
{
return convertName(legacyProgName, wa, null);
}
/**
* Returns the absolute prefix for a given path if it has one, or null otherwise.
*
* @param path
* The path for which we want to find the absolute prefix.
*
* @return The absolute prefix of the path or null if it doesn't have one.
*/
public static String getAbsolutePrefix(String path)
{
String aRoot;
for (int i = 0; i < legacyRoots.length; i++)
{
aRoot = legacyRoots[i];
if (aRoot.isEmpty())
{
// ensure empty entries are ignored
continue;
}
if (path.startsWith(aRoot))
{
return aRoot;
}
}
return null;
}
/**
* Remove the absolute prefix from a given path.
*
* @param absolutePath
* The absolute path which we want to transform into a path relative to the root folder.
*
* @param absolutePrefix
* The absolute prefix of the absolute path.
*
* @return The path relative to the root folder created from absolutePath
* using the {@link #canonicalize(String, boolean)} method.
*/
public static String removeAbsolutePrefix(String absolutePath, String absolutePrefix)
{
WorkArea wa = local.get();
String relName = absolutePath.substring(absolutePrefix.length());
if (relName.startsWith(fileSep) || relName.startsWith(altFileSep))
{
relName = relName.substring(1); // drop the starting fileSep
}
relName = canonicalize(relName, wa.caseSens);
return relName;
}
/**
* Static getter for the file separator.
*
* @return The {@link #fileSep} static data member.
*/
public static String getFileSep()
{
return fileSep;
}
/**
* Static getter for the array of Strings representing the propath paths.
*
* @return An array of String objects, each String being a path in the propath.
*/
public static String[] getPropath()
{
WorkArea wa = local.get();
if (wa.propath == null)
{
// just let it know the propath needs to be resolved - as it must be null originally
String searchPath = EnvironmentOps.getSearchPath().toStringMessage();
setSearchPath(searchPath);
}
return wa.propath;
}
/**
* Convert a Progress file name into Java class name. The input name can be an absolute file
* name, a file name relative to current location or a file name relative to some directory
* in a given list of directories. Before processing, the input file name is processed by {@link #canonicalize}.
* <p>
* A simple lookup is tried first. If this fails direct check for absolute path if attempted
* using one of the roots from {@code legacyRoots}. In multiple project scenario, the project
* name is also added as a path token in order to match the structure from
* {@code name_map.xml}.
* <p>
* If this mapping does not exist then each path segment of the list of directories is tried in order.
* The given program name is check to see if it is prefixed with the directory name, if so
* that prefix is removed and a lookup is done with this name. The first match found ends the
* search.
* <p>
* Absolute paths are converted into relative paths based on the existence of a matching path
* in the legacy PROPATH (see {@link EnvironmentOps#getLegacySearchPath}). To handle additional
* absolute paths, one must add the appropriate path prefix to the legacy PROPATH and it will
* be removed here.
* <p>
* This same PROPATH processing is how names relative to the current directory can be handled.
* This is the only way to handle that case since the current directory in the source system is
* runtime information that is not available here.
* <p>
* The legacy PROPATH is the key configuration tool by which one specifies to this method how
* to remove absolute and current directory relative prefixes. This means that the legacy
* PROPATH that is used here may need to be different than the PROPATH on the original system.
* <p>
* As a fallback, if no mapping can be found and if the given {@code progName} is a fully
* qualified Java class name which references a valid Java class, then the {@code progName} is
* returned unchanged.
* <p>
* In Windows it can verify the absolute or relative program name is in current PROPATH
* directory set. This allows to properly map the following program names used in
* PROGRESS RUN statement:
* "c:/path/to/project/stcases/uast/rundir/absolute/program.p"
* "rundir/../../uast/rundir/relative/test11.p."
* "../uast/rundir/relative/test5.p"
*
* @param legacyProgName
* Progress file name.
* @param lookup
* The lookup function used to resolve a program name to a {@link ExternalProgram}.
* @param wa
* WorkArea of the current local context.
* @param searchPaths
* Paths to use when searching for the Progress file. If null, will search in propath.
*
* @return A {@code String} array with three components:
* <ol>
* <li>Java class name for the given Progress file name as first element</li>
* <li>The legacy procedure name in second element of the array.</li>
* <li>The legacy qualified OO name (if the name represents a legacy class or
* interface).</li>
* </ol>
* If no appropriate mapping is present {@code null} is returned.
*/
private static String[] convertName(String legacyProgName,
Function<String, ExternalProgram> lookup,
WorkArea wa,
String[] searchPaths)
{
if (wa == null)
{
wa = local.get();
}
if (wa.propath == null)
{
// just let it know the propath needs to be resolved - as it must be null originally
String searchPath = EnvironmentOps.getSearchPath().toStringMessage();
setSearchPath(searchPath);
}
if (searchPaths == null)
{
searchPaths = wa.propath;
}
String progName = wa.caseSens ? legacyProgName : legacyProgName.toLowerCase();
String[] originalPropath = wa.originalPropath;
String retLegacy = legacyProgName;
ExternalProgram extProg = null;
// the searchPaths list may contain only relative paths (relative to the legacy conversion folder);
// if the given program name is an absolute path, it will use the 'legacyRoots' array to try
// to remove the absolute prefix, so that the remaining is a path which can be checked in the
// p2jMap. first found absolute root which allows the name to be resolved in the map will
// be used
// note: we can't determine in a os-independent way if ANY path is absolute or not. for
// this reason, the legacy-system/4gl-src-root directory.xml entry will always contain
// absolute path prefixes. if such a prefix matches, then it will be removed and the
// resulting path will be checked against the legacyName-to-Java mappings - if there exists
// a key matching this path, then this program will be returned.
// case 1. check if the progName represents an absolute filename of a source procedure
String absolutePrefix = getAbsolutePrefix(progName);
boolean hasAbsolutePrefix = absolutePrefix != null;
if(hasAbsolutePrefix)
{
String relName = progName.substring(absolutePrefix.length());
if (relName.startsWith(fileSep) || relName.startsWith(altFileSep))
{
relName = relName.substring(1); // drop the starting fileSep
}
relName = canonicalize(relName, wa.caseSens);
// regardless if there are '..' up paths or not, we must check the relName against
// our dictionary - don't use the searchPaths here.
extProg = lookup.apply(relName);
if (extProg == null)
{
// one more try, because of file collisions, the files from current project were
// converted from a 'token' directory of the src-root
String token = Utils.getProjectToken();
if (token != null)
{
extProg = lookup.apply(token + fileSep + relName);
}
}
if (extProg != null)
{
// the reported name is the full absolute name
retLegacy = legacyProgName;
}
}
if (hasAbsolutePrefix && extProg == null)
{
// absolute name, not found after removing the absolute prefixes, so fail
return null;
}
// case 2. search via the searchPaths
if (extProg == null && searchPaths.length != 0)
{
// we have determined that the name is not an absolute name; we will use the normal
// searchPaths search
for (int i = 0; i < searchPaths.length; i++)
{
String aPath = searchPaths[i];
if (aPath.isEmpty())
{
// ensure empty entries are ignored
continue;
}
String relName = aPath.concat(fileSep).concat(progName);
relName = removeUpPath(relName);
String aPathRel = removeUpPath(aPath);
// Check if the program is in searchPaths
if (relName.startsWith(aPathRel))
{
// this is a relative name, computed with a resolved searchPaths prefix; we must not
// remove the current searchPaths path, as we must match the exact program name as
// we found it in the (resolved) searchPaths list. in other words, searchPaths represents
// directories relative to the 'abl/' (legacy source deploy) folder or even an
// absolute folder.
// check for a match
extProg = lookup.apply(canonicalize(relName, wa.caseSens));
if (extProg != null)
{
if (!retLegacy.startsWith("."))
{
String path = originalPropath[i];
retLegacy = path +
(path.charAt(path.length() - 1) == fileSepChar ? "" : fileSepChar) +
legacyProgName;
}
break; // first match exits the loop
}
}
}
}
// case 3. check the simple form of the name - in this case, just check for an exact match
if (extProg == null)
{
extProg = lookup.apply(canonicalize(progName, wa.caseSens));
}
// case 4. fallback processing
if (extProg == null)
{
// enable support for fully qualified Java class names if they are passed in directly
Boolean isClass = CLASS_LOOKUPS.get(progName);
if (isClass == null)
{
try
{
Class.forName(progName);
// if we are here, then the progName must have been a fully qualified Java class name
// to a class that exists so we return it unchanged. The legacy name is unknown and
// unneeded/inexistent.
CLASS_LOOKUPS.put(progName, Boolean.TRUE);
return new String[]{progName, null, null};
}
catch (ClassNotFoundException cnfe)
{
CLASS_LOOKUPS.put(progName, Boolean.FALSE);
// do nothing, the given program can't be found as a Java class
}
}
else if (isClass == Boolean.TRUE)
{
return new String[] {progName, null, null};
}
}
if (extProg == null)
{
// search failed. The parameter does not contain a valid procedure name
return null;
}
if (wa.isWindows)
{
// 4GL automatically switch all slashes into backslashes on Win OS
retLegacy = fileSepRegexp.matcher(retLegacy).replaceAll("\\\\");
}
return new String[] { addPkg(extProg.jname), retLegacy, extProg.ooname };
}
/**
* Get the {@link ExternalProgram} of the given Legacy 4GL program name.
*
* @param pname
* Legacy 4GL program name.
*
* @return the {@link ExternalProgram} or {@code null} if not a valid pname.
*/
private static ExternalProgram getExternalProgram(String pname)
{
ExternalProgram extProg = null;
if (pname != null)
{
boolean caseSens = getLegacyCaseSensitive();
extProg = caseSens ? p2jMap.get(pname) : p2jMap_ci.get(pname);
}
return extProg;
}
/**
* Notification that the search path has changed - it updates the {@link WorkArea#propath}
* value.
*
* @param searchPath
* The new PROPATH value.
*/
private static void setSearchPath(String searchPath)
{
String[][] searchResult;
synchronized (searchPathCache)
{
searchResult = searchPathCache.get(searchPath);
}
if (searchResult != null)
{
WorkArea wa = local.get();
wa.propath = searchResult[0];
wa.originalPropath = searchResult[1];
wa.propathHash = (long) Arrays.hashCode(searchResult[0]);
return;
}
String originalSearchPath = searchPath;
boolean isWindows = EnvironmentOps.isLegacyPlatformWindows();
boolean caseSens = getLegacyCaseSensitive();
// get rid of the Windows file separator in working strings
if (isWindows)
{
searchPath = dblBackslashRegexp.matcher(searchPath).replaceAll(fileSep);
}
// internally, we use the canonicalized paths (lowercased and / for path separator)
// but for the result, we use the original case and path separator (as 4GL does)
String[] originalPropath = searchPath.split(EnvironmentOps.PROPATH_SEPARATOR);
String[] propath;
if (!caseSens)
{
propath = new String[originalPropath.length];
for (int i = 0; i < originalPropath.length; i++)
{
propath[i] = originalPropath[i].toLowerCase();
}
}
else
{
propath = originalPropath;
}
// remove the absolute root (if possible) from this entry
for (int i = 0; i < propath.length; i++)
{
String aPath = propath[i];
for (String aRoot : legacyRoots)
{
if (aPath.startsWith(aRoot))
{
aPath = aPath.substring(aRoot.length());
if (aPath.startsWith(fileSep))
{
aPath = aPath.substring(1);
}
break;
}
}
propath[i] = aPath;
}
WorkArea wa = local.get();
wa.propath = propath;
wa.originalPropath = originalPropath;
wa.propathHash = (long) Arrays.hashCode(propath);
synchronized (searchPathCache)
{
searchPathCache.put(originalSearchPath, new String[][] { wa.propath, wa.originalPropath });
}
}
/**
* Private utility method for locating a parameter of a routine.
*
* @param pname
* Legacy 4GL program name.
* @param iename
* Legacy internal-entry name.
* @param func
* Flag indicating if this is a function or a procedure.
* @param param
* 0-based index of the parameter to be checked.
*
* @return the requested parameter or {@code null} if parameters do not point to a valid
* parameter.
*/
private static Parameter getParameter(String pname, String iename, boolean func, int param)
{
InternalEntry ie = getInternalEntry(pname, iename, func);
if (ie == null || param < 0 || param >= ie.getParameterListSize())
{
return null;
}
return ie.getParameter(param);
}
/**
* Given a program name, replace all folder names which have an associated 'source' folder,
* and return the new path.
*
* @param pname
* The legacy program name.
*
* @return The resolved program names or empty array if no change was possible.
*/
private static String[] resolvePathAliases(String pname)
{
Map<String,String> pA = getLegacyCaseSensitive() ? pathAliases : pathAliases_ci;
return resolvePathAliases(pA, pname);
}
/**
* Load the path aliases from the directory, into the {@link #pathAliases} map. This map
* contains as key the 'aliased' name which is an alias for a 'source' folder.
* <p>
* The directory contains these mappings in the <code>/server/default/path-aliases</code> or
* <code>/server/<server-id>/path-aliases</code> node. The child nodes have as class
* <code>path-alias</code> and this structure:
* <code>
* <node class="path-alias" name="alias1">
* <br>
* <node-attribute name="source" value="src1"/>
* <br>
* <node-attribute name="alias" value="obj1"/>
* <br>
* </node>
* </code>
* <p>
* The <code>alias1</code> name is just an unique ID for each child.
*/
private static void loadPathAliases()
{
DirectoryService ds = DirectoryService.getInstance();
if (ds == null)
{
// we are in conversion mode, we can't use path aliases
return;
}
if (!ds.bind())
throw new RuntimeException("directory bind failed");
try
{
String path = Utils.findDirectoryNodePath(ds, "path-aliases", "container", false);
if (path == null)
{
return;
}
String[] aliasPaths = ds.enumerateNodes(path);
if (aliasPaths.length == 0)
{
return;
}
pathAliases = new HashMap<>();
pathAliases_ci = new HashMap<>();
for (String aliasPath : aliasPaths)
{
aliasPath = path + "/" + aliasPath;
String source = ds.getNodeString(aliasPath, "source");
String alias = ds.getNodeString(aliasPath, "alias");
String source_ci = source.toLowerCase();
String alias_ci = alias.toLowerCase();
pathAliases.put(alias, source);
pathAliases_ci.put(alias_ci, source_ci);
}
}
finally
{
ds.unbind();
}
}
/**
* Given this external program (which may be associated with a legacy OO class), register all
* of its exposed services.
*
* @param extProg
* The external program specification.
* @param qname
* Fully qualified, converted, Java class name.
*/
private static void registerServices(ExternalProgram extProg, String qname)
{
Class<?> cls = null;
try
{
cls = Class.forName(qname);
}
catch (ClassNotFoundException e)
{
throw new RuntimeException(e);
}
boolean foundService = false;
LegacyService[] legacyServices = cls.getAnnotationsByType(LegacyService.class);
if (legacyServices != null)
{
for (LegacyService pls : legacyServices)
{
String executionMode = pls.executionMode().toLowerCase();
if (LegacyService.REST.equalsIgnoreCase(pls.type()))
{
if (executionMode.isEmpty())
{
throw new RuntimeException("executionMode must be specified for " + qname);
}
}
if (!pls.name().isEmpty())
{
switch (pls.type().toUpperCase())
{
case LegacyService.SOAP:
SoapHandler.addService(pls, extProg.pname, extProg.main.getMethod());
break;
case LegacyService.REST:
RestHandler.addService(pls, extProg.pname, extProg.main.getMethod());
break;
case LegacyService.WEBHANDLER:
WebServiceHandler.addService(pls, (Class) cls);
break;
default:
throw new RuntimeException("Unknown legacy service type " + pls.type() +
" in " + qname);
}
foundService = true;
}
if (pls.name().isEmpty() ||
(pls.type().equalsIgnoreCase(LegacyService.SOAP) &&
executionMode.equalsIgnoreCase(LegacyWebServiceHandler.PERSISTENT)))
{
// this is a legacy class or external program, get the methods
boolean isCls = BaseObject.class.isAssignableFrom(cls);
boolean isJava = executionMode.equalsIgnoreCase(LegacyWebServiceHandler.JAVA);
Method[] methods = cls.getDeclaredMethods();
for (Method m : methods)
{
LegacyService[] mLegacyServices = m.getAnnotationsByType(LegacyService.class);
if (mLegacyServices != null)
{
for (LegacyService mls : mLegacyServices)
{
switch (mls.type().toUpperCase())
{
case LegacyService.SOAP:
if (isJava)
{
throw new RuntimeException("Java SOAP services are not supported!");
}
else if (isCls)
{
SoapHandler.addService(pls, (Class) cls, mls, m);
}
else
{
boolean isFunc = (m.getReturnType() != void.class);
InternalEntry ie = (isFunc ? extProg.j2pf : extProg.j2pp).get(m.getName());
SoapHandler.addService(pls, extProg.pname, mls, ie, isFunc);
}
break;
case LegacyService.REST:
if (isJava)
{
Class<?>[] parametersTypes = m.getParameterTypes();
boolean erasedTypes = (parametersTypes.length > 0) && Arrays.stream(parametersTypes).allMatch(Object.class::equals);
if (!erasedTypes)
{
RestHandler.addJavaService(pls, (Class) cls, mls, m);
}
}
else if (isCls)
{
RestHandler.addService(pls, (Class) cls, mls, m);
}
else
{
boolean isFunc = (m.getReturnType() != void.class);
InternalEntry ie = (isFunc ? extProg.j2pf : extProg.j2pp).get(m.getName());
RestHandler.addService(pls, extProg.pname, mls, ie, isFunc);
}
break;
default:
throw new RuntimeException("Unknown legacy service type " + mls.type() +
" in " + qname);
}
foundService = true;
}
}
}
}
}
}
if (!foundService && LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER, "Could not find a LegacyService annotation in " + qname + "!");
}
}
/**
* This method loads all mapping data from a URL which is calculated
* using the <code>pkgroot</code> and the {@link #NAME_MAP_FILE} with
* all "." characters converted to file separators and an extra file
* separator added before the file name portion. This URL is expected to
* be found in the application's JAR file. Note that this will silently
* return if the mapping data is not found. Any file system configuration
* data will be read from the directory via the {@link EnvironmentOps}
* helper.
*/
private static void initMappingData()
{
// prevent reinitialization
if (mappingDataInitialized)
{
return;
}
// allow only one thread to continue, other threads shall wait
synchronized (SourceNameMapper.class)
{
// prevent recursive calls and reinitialization
if (mappingDataInitialized || mappingDataInitInProgess)
{
return;
}
// prevent recursive calls
mappingDataInitInProgess = true;
if (Configuration.isRuntimeConfig())
{
classMap = new ConcurrentHashMap<>(1024);
}
p2jMap = new HashMap<>(1024);
p2jMap_ci = new CaseInsensitiveHashMap<>(1024);
p2jNonExtMap = new HashMap<>(1024);
p2jNonExtMap_ci = new CaseInsensitiveHashMap<>(1024);
pathLookups = new HashMap<>();
j2pMap = new HashMap<>(1024);
searchPathCache = CacheManager.createLRUCache(SourceNameMapper.class, "search", 64);
// lookup the descriptors for the original file system
pkgroot = Utils.getDirectoryNodeString(null, "legacy-system/pkgroot", "");
// if the pkgroot is null get the custom package and reset it
if (pkgroot == null || pkgroot.isEmpty())
{
pkgroot = custpkgroot;
custpkgroot = null;
}
// this is only a conversion-time configuration value ("basepath")
proroot = "";
boolean caseSens = getLegacyCaseSensitive();
String pathSep = getLegacyPathSeparator();
// at runtime we use legacyRoots - all must be absolute paths
String srcRoots = Utils.getDirectoryNodeString(null, "legacy-system/4gl-src-root", "");
if (!caseSens)
{
srcRoots = srcRoots.toLowerCase();
}
// get rid of the Windows file separator in working strings
if (EnvironmentOps.isLegacyPlatformWindows())
{
srcRoots = dblBackslashRegexp.matcher(srcRoots).replaceAll(fileSep);
}
legacyRoots = srcRoots.split(pathSep);
String nameMappingXmlPath = Utils.packageToJarPath(pkgroot) + NAME_MAP_FILE;
Set<URL> allNameMaps = new HashSet<>();
ClassLoader systemClassLoader = MultiClassLoader.getClassLoader();
for (String jarPath : JarUtil.resolveClasspathJars(LOG))
{
JarClassLoader loader = JarClassLoader.createJarClassLoader(jarPath, systemClassLoader);
if (loader.containsResource(nameMappingXmlPath))
{
URL resourceUrl = loader.findResource(nameMappingXmlPath);
if (resourceUrl != null)
{
allNameMaps.add(resourceUrl);
}
}
}
if (allNameMaps.isEmpty())
{
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE,
"No mapping data exists: " + NAME_MAP_FILE + " not found in jars.\n\t" +
"Expected location: " + nameMappingXmlPath);
}
URL resource = null;
try
{
resource = systemClassLoader.getResource(nameMappingXmlPath);
}
catch (Exception e)
{
// silently ignore it
}
if (resource != null)
{
LOG.log(Level.SEVERE, "Developper mode: using file-system resource " + resource);
allNameMaps.add(resource);
}
else
{
return;
}
}
allowedLegacyExtensions = new HashSet<>();
// always allow compiled procedure code files
allowedLegacyExtensions.add("r");
allowedLegacyExtensions.add("R");
boolean hasLoadedAnyMap = false;
for (URL mapUrl : allNameMaps)
{
try
{
populateInMemoryMaps(mapUrl, nameMappingXmlPath);
hasLoadedAnyMap = true;
}
catch (Exception e)
{
LOG.warning("Unable to load name mapping data from " + mapUrl.toExternalForm(), e);
}
}
if (!hasLoadedAnyMap)
{
throw new RuntimeException("Unable to load any name mapping data.");
}
loadPathAliases();
mappingDataInitialized = true;
mappingDataInitInProgess = false;
}
}
/**
* Reads the name map xml files from the provided URL and resolves all mappings. Should always get
* called in a synchronized block.
*
* @param mapUrl
* The URL of the map name xml file (the file path inside the jar).
* @param nameMappingXmlPath
* Only the name map xml file path.
*
* @throws IOException
* Map name xml file read exception.
* @throws ParserConfigurationException
* XML parser configuration exception.
* @throws SAXException
* XML parser general exception.
*/
private static void populateInMemoryMaps(URL mapUrl, String nameMappingXmlPath)
throws IOException,
ParserConfigurationException,
SAXException
{
Document dom = XmlHelper.parse(mapUrl.openStream());
// find the root node
Element root = dom.getDocumentElement();
if (root == null)
{
LOG.warning("Name mapping file " + nameMappingXmlPath + " is empty.");
return;
}
populateRestServices(root);
if (!populateClasses(root))
{
LOG.warning("No class mappings in name map " + mapUrl);
return;
}
populateInitPaths(root, mapUrl);
}
/**
* Resolves and populates in-memory the REST service mappings from the xml root node. Should always get
* called in a synchronized block.
*
* @param mapRootNode
* The root node of the map name xml file.
*/
private static void populateRestServices(Element mapRootNode)
{
// enumerate all 'rest-service' nodes.
NodeList restServices = mapRootNode.getElementsByTagName(ELEM_REST_SERVICE);
if (restServices == null || restServices.getLength() == 0)
{
return;
}
if (servicePrograms == null)
{
servicePrograms = new ArrayList<>();
}
for (int i = 0; i < restServices.getLength(); i++)
{
Element restService = (Element) restServices.item(i);
String jname = restService.getAttribute(ATTR_JNAME);
// create a bogus pname
String pname = jname + "-fwd-rest-service.p";
String ooname = restService.getAttribute(OONAME);
ExternalProgram ep = buildExternalProgram(pname, jname, ooname, restService);
servicePrograms.add(ep);
}
}
/**
* Resolves and populates in-memory the class mappings from the xml root node. Should always get called
* in a synchronized block.
*
* @param mapRootNode
* The root node of the map name xml file.
*
* @return <code>true</code> if the node is successfully read, <code>false</code> otherwise.
*
* @throws IllegalStateException
* On malformed class mapping entry.
*/
private static boolean populateClasses(Element mapRootNode)
{
// enumerate all child nodes (there will be one for each source file that can be mapped)
NodeList cmaps = mapRootNode.getElementsByTagName(ELEM_CMAP);
if (cmaps == null)
{
return false;
}
for (int i = 0; i < cmaps.getLength(); i++)
{
Element classMap = (Element) cmaps.item(i);
String pname = classMap.getAttribute(ATTR_PNAME);
String jname = classMap.getAttribute(ATTR_JNAME);
String ooname = classMap.getAttribute(OONAME);
if (pname == null || pname.length() == 0 || jname == null || jname.length() == 0)
{
throw new IllegalStateException("Malformed class mapping entry for " + classMap.toString());
}
if (j2pMap != null && j2pMap.containsKey(jname))
{
ExternalProgram resolvedProgram = j2pMap.get(jname);
if (!resolvedProgram.pname.equals(pname) ||
(!(ooname.isEmpty() && resolvedProgram.ooname == null) && !ooname.equals(resolvedProgram.ooname)))
{
LOG.severe("Mapping for class " + jname + " found multiple times with different attributes.");
System.exit(-1);
}
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("Mapping for class " + jname + " found multiple times.");
}
}
// store bidirectional mappings for the progress relative
// filename to/from the java relative file name mapping
ExternalProgram ep = buildExternalProgram(pname, jname, ooname, classMap);
boolean withServices = "true".equalsIgnoreCase(classMap.getAttribute(WITH_SERVICES));
if (withServices)
{
if (servicePrograms == null)
{
servicePrograms = new ArrayList<>();
}
servicePrograms.add(ep);
}
// Each program name will create a SinglePathLookup, but if the unqualified
// name appears multiple times, it will create a new MultiPathLookup instance
// or use an existing one.
SinglePathLookup singleLookup = new SinglePathLookup(pname);
String simpleProgramName = singleLookup.getSimpleProgramName();
if (!pathLookups.containsKey(simpleProgramName))
{
pathLookups.put(simpleProgramName, singleLookup);
}
else
{
PathLookup pathLookup = pathLookups.get(simpleProgramName);
if (pathLookup instanceof SinglePathLookup)
{
MultiPathLookup multiLookup = new MultiPathLookup();
multiLookup.references.add((SinglePathLookup) pathLookup);
multiLookup.references.add(singleLookup);
pathLookups.put(simpleProgramName, multiLookup);
}
else
{
((MultiPathLookup) pathLookup).references.add(singleLookup);
}
}
p2jMap.put(ep.pname, ep);
p2jMap_ci.put(pname, ep);
j2pMap.put(ep.jname, ep);
if (ep.pnameExt != null && !allowedLegacyExtensions.contains(ep.pnameExt))
{
allowedLegacyExtensions.add(ep.pnameExt);
allowedLegacyExtensions.add(ep.pnameExt.toLowerCase());
}
List<ExternalProgram> samePnameProgs = p2jNonExtMap.get(ep.pnameNoExt);
if (samePnameProgs == null)
{
samePnameProgs = new ArrayList<>(1);
p2jNonExtMap.put(ep.pnameNoExt, samePnameProgs);
p2jNonExtMap_ci.put(ep.pnameNoExt, samePnameProgs);
}
samePnameProgs.add(ep);
}
return true;
}
/**
* Resolves and populates in-memory the initialization paths from the xml root node. Should always get
* called in a synchronized block.
*
* @param mapRootNode
* The root node of the map name xml file.
* @param mapUrl
* The URL of the map name xml file (the file path inside the jar). Used for logging.
*/
private static void populateInitPaths(Element mapRootNode, URL mapUrl)
{
if (!PATH_INIT_THREADS_COUNTER.compareAndSet(-1, 1))
{
PATH_INIT_THREADS_COUNTER.incrementAndGet();
}
if (initializationPaths == null)
{
initializationPaths = new ConcurrentHashMap<>();
}
Runnable task = () ->
{
NodeList pmaps = mapRootNode.getElementsByTagName(ELEM_PMAP);
int plen = pmaps.getLength();
for (int i = 0; i < plen; i++)
{
Element pathEntry = (Element) pmaps.item(i);
String pathName = pathEntry.getAttribute(ATTR_PATHNAME);
if (pathName == null || pathName.length() == 0)
{
throw new IllegalStateException("Malformed path mapping entry for " + pathEntry.toString() + " found in " + mapUrl);
}
if (initializationPaths.containsKey(pathName))
{
LOG.log(Level.FINE,
"Path %s already initialized. Skipping element in name map %s.",
pathName,
mapUrl);
continue;
}
initializationPaths.put(pathName, MAPPING_COUNTER.getAndIncrement());
}
sourceCache = CacheManager.createLRUCache(SourceNameMapper.class, "source", 8);
PATH_INIT_THREADS_COUNTER.decrementAndGet();
};
Thread pathMappingThread = new Thread(task);
pathMappingThread.setDaemon(true);
pathMappingThread.setName("Path mapping initializer thread for " + mapUrl);
pathMappingThread.start();
}
/**
* Returns <code>true</code> if all path mapping initializer threads have completed work,
* <code>false</code> otherwise.
*
* @return See above.
*/
private static boolean isPathMappingInitialized()
{
// mappingDataInitialized indicates all jars have been processed
return mappingDataInitialized && PATH_INIT_THREADS_COUNTER.get() == 0;
}
/**
* Collect the {@link ExternalProgram} info contained in the <code>classMap
* </code> node.
*
* @param pname
* The legacy 4GL program name.
* @param jname
* The converted class name.
* @param ooname
* The legacy OO class name.
* @param classMap
* The DOM node containing the info for this external program.
*
* @return An {@link ExternalProgram} instance for this DOM node.
*/
private static ExternalProgram buildExternalProgram(String pname,
String jname,
String ooname,
Element classMap)
{
String publishedEvents = classMap.getAttribute(PUBLISHED_EVENTS);
ExternalProgram extProg = new ExternalProgram(pname, jname, publishedEvents);
extProg.ooname = ooname.isEmpty() ? null : ooname;
// for legacy classes, just the class/interface registry is kept plus any function declarations.
// the methods are annotated with their signature, so build them from there.
// the legacy classes, the LegacyClass structure is built when the class is first access at runtime
// for external programs, both LegacySignature and name_map.xml are kept, but they are built on a
// lazy approach
// enumerate method mapping nodes
NodeList mmaps = classMap.getElementsByTagName(ELEM_MMAP);
if (mmaps == null)
{
return extProg;
}
int mlen = mmaps.getLength();
if (mlen > 0)
{
extProg.ieMap = new LinkedHashMap<>();
}
Set<String> defFuncs = new HashSet<>();
Set<String> forwardFuncs = new HashSet<>();
// process each method mapping
// 4GL allows same name for a function or procedure, as long as they
// are not internal. they appear in the internal-entry list, but the
// get-signature call will see only the function (for this "feature" to
// work, the procedure must be defined before the function, else 4GL
// gets compile error).
for (int j = 0; j < mlen; j++)
{
Element methMap = (Element) mmaps.item(j);
String iepname = methMap.getAttribute(ATTR_PNAME);
String iejname = methMap.getAttribute(ATTR_JNAME);
if (iepname == null || iejname == null || iejname.length() == 0 || iepname.length() == 0)
{
String errmsg = "Malformed method mapping entry for " + methMap.toString();
throw new IllegalStateException(errmsg);
}
InternalEntry ie = buildMethod(iepname, iejname, methMap);
ie.setExternalProgram(extProg);
// the FORWARD functions are emitted in the name_map.xml only to preserve the order.
if (ie.isFunction())
{
String forward = methMap.getAttribute("forward");
if ("true".equalsIgnoreCase(forward))
{
forwardFuncs.add(iepname);
}
else
{
defFuncs.add(iepname);
// if the function already exists as forward, take the parameter
// names from here and update the forward declaration
if (forwardFuncs.contains(iepname))
{
if (ie.getParameterListSize() > 0)
{
InternalEntry ief = extProg.search(iepname, true);
ief.setParameterList(ie.getParameterList());
}
continue;
}
}
}
// store the bidirectional Progress relative proc/function name from/to Java method name
// mapping
InternalEntryKey key = new InternalEntryKey(ie.isFunction(), ie.getLegacyName());
extProg.ieMap.put(key, ie);
Map<String, InternalEntry> p2j = null;
Map<String, InternalEntry> j2p = null;
if (ie.isFunction())
{
if (extProg.p2jf.isEmpty())
{
extProg.p2jf = new HashMap<>();
extProg.j2pf = new HashMap<>();
}
p2j = extProg.p2jf;
j2p = extProg.j2pf;
}
else
{
if (extProg.p2jp.isEmpty())
{
extProg.p2jp = new HashMap<>();
extProg.j2pp = new HashMap<>();
}
p2j = extProg.p2jp;
j2p = extProg.j2pp;
}
p2j.put(iepname.toLowerCase().intern(), ie);
j2p.put(ie.jname, ie);
}
// remove all functions defined as forward but without body
forwardFuncs.removeAll(defFuncs);
for (String iepname : forwardFuncs)
{
InternalEntry ie = extProg.search(iepname, true);
InternalEntryKey key = new InternalEntryKey(ie.isFunction(), ie.getLegacyName());
extProg.ieMap.remove(key);
}
extProg.main = new InternalEntry("", "", InternalEntry.Type.MAIN);
extProg.main.setExternalProgram(extProg);
buildParameters(classMap, extProg.main);
extProg.main.initialize();
return extProg;
}
/**
* Collect the {@link InternalEntry} info contained in the <code>methMap</code> node.
*
* @param pname
* The legacy 4GL internal-entry name.
* @param jname
* The converted internal-entry name.
* @param methMap
* The DOM node containing the info for this internal-entry.
*
* @return An {@link InternalEntry} instance for this DOM node.
*/
private static InternalEntry buildMethod(String pname, String jname, Element methMap)
{
String type = methMap.getAttribute(ATTR_TYPE);
if (type == null || type.length() == 0)
{
String err = "Missing type attribute for method mapping entry " + methMap.toString();
throw new IllegalStateException(err);
}
InternalEntry ie = null;
if (InternalEntry.Type.DLL_ENTRY.toString().equals(type))
{
// instantiate the subclass that handles native calls
ie = new NativeAPIEntry(pname, jname);
}
else
{
// the common case
ie = new InternalEntry(pname, jname, type);
}
buildParameters(methMap, ie);
int len = methMap.getAttributes().getLength();
// add the method's attributes
for (int i = 0; i < len; i++)
{
Node n = methMap.getAttributes().item(i);
String name = n.getNodeName();
String value = n.getNodeValue();
ie.putAttribute(name, value);
}
ie.initialize();
return ie;
}
/**
* Build the parameters for the specified internal entry.
*
* @param node
* The current node from where the parameters are read.
* @param ie
* The internal entry where to save the parameters.
*/
private static void buildParameters(Element node, InternalEntry ie)
{
// this enumerates all parameter nodes under the node instance (on any
// level)
NodeList params = node.getElementsByTagName(ELEM_PARAM);
if (params == null)
{
return;
}
int plen = params.getLength();
ArrayList<Parameter> list = new ArrayList<>();
// process each method mapping
for (int j = 0; j < plen; j++)
{
Element param = (Element) params.item(j);
// keep only parameters which belong to the given node
if (param.getParentNode() != node)
{
continue;
}
String ppname = param.getAttribute(ATTR_PNAME);
String ptype = param.getAttribute(ATTR_TYPE);
String pmode = param.getAttribute(ATTR_PARAM_MODE);
if (!("TABLE".equalsIgnoreCase(ptype) ||
"BUFFER".equalsIgnoreCase(ptype) ||
"DATASET".equalsIgnoreCase(ptype)) &&
(ppname == null || ppname.length() == 0))
{
String errmsg = "Malformed parameter entry for " + param.toString() +
", in method " + node.toString();
throw new IllegalStateException(errmsg);
}
Parameter p = new Parameter(ppname, ptype, pmode);
// add the parameter's attributes
int attrLen = param.getAttributes().getLength();
for (int i = 0; i < attrLen; i++)
{
Node n = param.getAttributes().item(i);
String attrName = n.getNodeName();
String attrValue = n.getNodeValue();
p.putAttribute(attrName, attrValue);
}
p.setQualified(p.getAttribute("qualified"));
p.setExtent(p.getAttribute("extent"));
list.add(p);
}
if (plen > 0)
{
list.trimToSize();
ie.setParameterList(list);
}
}
/**
* Build the internal-entries 4GL attribute for the given external program.
*
* @param extProg
* The {@link ExternalProgram} instance.
* @param prv
* Flag to include or not the private members.
*
* @return See above.
*/
private static String buildInternalEntries(ExternalProgram extProg, boolean prv)
{
// - PRIVATE members are not visible
// - prototypes (such as IN SUPER or IN handle functions/procs) appear in this list
// - FORWARD functions without implementations do not appear
// - first are procedures, then functions, then IN SUPER procedures are put last (in
// reverse order)
StringBuilder sbSuperProcs = new StringBuilder();
StringBuilder sbProcs = new StringBuilder();
StringBuilder sbFuncs = new StringBuilder();
List<String> superProcs = new LinkedList<>();
for (InternalEntry ie : extProg.ieMap.values())
{
StringBuilder sb = null;
if (ie.isPrivate() && !prv)
{
continue;
}
if (ie.isFunction())
{
sb = sbFuncs;
}
else
{
if (ie.isSuper())
{
superProcs.add(0, ie.pname);
continue;
}
sb = sbProcs;
}
if (sb.length() > 0)
{
sb.append(',');
}
sb.append(ie.pname);
}
for (String psuper : superProcs)
{
if (sbSuperProcs.length() > 0)
{
sbSuperProcs.append(',');
}
sbSuperProcs.append(psuper);
}
StringBuilder sb = new StringBuilder();
sb.append(sbProcs);
if (sb.length() > 0 && sbFuncs.length() > 0)
{
sb.append(',');
}
sb.append(sbFuncs);
if (sb.length() > 0 && sbSuperProcs.length() > 0)
{
sb.append(',');
}
sb.append(sbSuperProcs);
return sb.toString();
}
/**
* Build a map of legacy method names to their Java {@link Method}, for the given external program.
* <p>
* Only methods marked with {@link LegacySignature} are included.
*/
static void buildLegacyProgram(ExternalProgram extprog)
{
Class<?> cls = null;
try
{
cls = Class.forName(addPkg(extprog.jname));
}
catch (ClassNotFoundException e)
{
throw new RuntimeException(e);
}
Method[] methods = cls.getDeclaredMethods();
for (Method mthd : methods)
{
if (!mthd.isSynthetic() && mthd.isAnnotationPresent(LegacySignature.class))
{
LegacySignature ls = mthd.getAnnotation(LegacySignature.class);
InternalEntry ie = null;
if (ls.type() == InternalEntry.Type.MAIN)
{
ie = extprog.main;
}
else if (ls.type() == InternalEntry.Type.FUNCTION)
{
ie = extprog.j2pf.get(mthd.getName());
}
else if (ls.type() == InternalEntry.Type.PROCEDURE)
{
ie = extprog.j2pp.get(mthd.getName());
}
if (ie != null)
{
ie.setMethod(mthd);
}
}
}
}
/**
* Build a legacy class entry by resolving all the legacy methods (marked with the
* {@link LegacySignature} annotation) and register them.
* <p>
* The {@link InternalEntry} will be linked with the defining Java method.
*
* @param cls
* The Java class associated with the legacy 4GL type.
*
* @return The built legacy class.
*/
static LegacyClass buildLegacyClass(Class<?> cls)
{
LegacyClass lcls = new LegacyClass();
Method[] methods = cls.getDeclaredMethods();
for (Method mthd : methods)
{
if (!mthd.isSynthetic() && mthd.isAnnotationPresent(LegacySignature.class))
{
LegacySignature ls = mthd.getAnnotation(LegacySignature.class);
if (ls.type() == InternalEntry.Type.METHOD ||
ls.type() == InternalEntry.Type.GETTER ||
ls.type() == InternalEntry.Type.SETTER)
{
InternalEntry ie = buildInternalEntry(ls, mthd.getName());
ie.setMethod(mthd);
String key = ie.pname.toLowerCase().intern();
Map<String, List<InternalEntry>> target =
ls.type() == InternalEntry.Type.GETTER
? lcls.getters
: (ls.type() == InternalEntry.Type.SETTER
? lcls.setters
: lcls.overloads);
List<InternalEntry> overloads = target.get(key);
if (overloads == null)
{
target.put(key, overloads = new LinkedList<>());
}
overloads.add(ie);
}
else if (ls.type() == InternalEntry.Type.CONSTRUCTOR)
{
InternalEntry ie = buildInternalEntry(ls, mthd.getName());
ie.setMethod(mthd);
if (Modifier.isStatic(mthd.getModifiers()))
{
if (lcls.staticCtor != null)
{
throw new IllegalStateException("More than one static constructor defined for " + cls);
}
lcls.staticCtor = ie;
}
else
{
lcls.ctors.add(ie);
}
}
else if (ls.type() == InternalEntry.Type.DESTRUCTOR)
{
InternalEntry ie = buildInternalEntry(ls, mthd.getName());
ie.setMethod(mthd);
if (lcls.dtor != null)
{
throw new IllegalStateException("More than one static constructor defined for " + cls);
}
lcls.dtor = ie;
}
}
}
for (Field f : cls.getDeclaredFields())
{
if (!f.isSynthetic() && f.isAnnotationPresent(LegacySignature.class))
{
LegacySignature sig = f.getAnnotation(LegacySignature.class);
if (sig.type() == InternalEntry.Type.PROPERTY && sig.extent() != SourceNameMapper.NO_EXTENT)
{
lcls.extents.put(sig.name(), sig.extent());
}
}
}
lcls.overloads = Collections.unmodifiableMap(lcls.overloads);
lcls.getters = Collections.unmodifiableMap(lcls.getters);
lcls.setters = Collections.unmodifiableMap(lcls.setters);
lcls.extents = Collections.unmodifiableMap(lcls.extents);
// build the destructor list
Class<?> parent = cls;
while (BaseObject.class.isAssignableFrom(parent))
{
LegacyClass pcls;
if (parent == cls)
{
pcls = lcls;
}
else
{
registerLegacyClass(parent);
pcls = classMap.get(parent);
}
if (pcls.dtor != null)
{
if (lcls.dtors == null)
{
lcls.dtors = new LinkedList<>();
}
lcls.dtors.add(0, pcls.dtor);
}
parent = parent.getSuperclass();
}
lcls.executeMethods = resolveExecuteMethods(cls);
if (lcls.executeMethods.length == 0)
{
lcls.executeMethods = null;
}
return lcls;
}
/**
* Build an {@link InternalEntry} matching the specified legacy signature.
*
* @param ls
* The signature.
* @param jmeth
* The converted Java method.
*
* @return See above.
*/
public static InternalEntry buildInternalEntry(LegacySignature ls, Method jmeth)
{
if (ls == null || jmeth == null)
{
return null;
}
InternalEntry ie = buildInternalEntry(ls, jmeth.getName());
ie.setMethod(jmeth);
return ie;
}
/**
* Check whether a {@link Class} is either a legacy class that inherits from {@link _BaseObject_} or is
* converted class.
*
* @param clazz the {@link Class} to be tested
* @return See above
*/
public static boolean isLegacyOrConvertedClass(Class clazz)
{
return _BaseObject_.class.isAssignableFrom(clazz) || // is legacy
SourceNameMapper.getLegacySourceName(clazz.getName()) != null; // is a converted class
}
/**
* Build an {@link InternalEntry} matching the specified legacy signature.
*
* @param ls
* The signature.
* @param jname
* The converted Java name.
*
* @return See above.
*/
static InternalEntry buildInternalEntry(LegacySignature ls, String jname)
{
String name = (ls.name().isEmpty() ? jname : ls.name());
InternalEntry ie = new InternalEntry(name, jname, ls.type());
if (ls.extent() != NO_EXTENT)
{
ie.putAttribute("extent", Integer.toString(ls.extent()));
}
if (!ls.qualified().isEmpty())
{
ie.putAttribute("qualified", ls.qualified());
}
else if ("object".equalsIgnoreCase(ls.returns()))
{
ie.putAttribute("qualified", "progress.lang.object");
}
List<Parameter> parameters = new LinkedList<>();
for (LegacyParameter lp : ls.parameters())
{
parameters.add(buildParameter(lp));
}
ie.setParameterList(parameters);
ie.initialize();
return ie;
}
/**
* Build a {@link Parameter} instance from the specified {@link LegacyParameter} annotation.
*
* @param lp
* The parameter signature.
*
* @return See above.
*/
private static Parameter buildParameter(LegacyParameter lp)
{
Parameter param = new Parameter(lp.name(), lp.type(), lp.mode());
if (!lp.qualified().isEmpty())
{
param.setQualified(lp.qualified());
param.putAttribute("qualified", lp.qualified());
}
else if ("object".equalsIgnoreCase(lp.type()))
{
param.setQualified("progress.lang.object");
param.putAttribute("qualified", "progress.lang.object");
}
if (lp.extent() != NO_EXTENT)
{
param.setExtent(Integer.toString(lp.extent()));
param.putAttribute("extent", Integer.toString(lp.extent()));
}
else
{
param.setExtent(null);
}
if (!lp.table().isEmpty())
{
param.putAttribute("table", lp.table());
}
if (!lp.buffer().isEmpty())
{
param.putAttribute("buffer", lp.buffer());
}
if (!lp.bufferFor().isEmpty())
{
param.putAttribute("for", lp.bufferFor());
}
if (lp.append())
{
param.putAttribute("append", Boolean.toString(lp.append()));
}
if (lp.bind())
{
param.putAttribute("bind", Boolean.toString(lp.bind()));
}
if (lp.handleTo())
{
param.putAttribute("handle_to", Boolean.toString(lp.handleTo()));
}
return param;
}
/**
* Construct a full class name (including packages) for the stored package
* root and the given class name.
*
* @param name
* Class name.
*
* @return The fully qualified class name.
*/
private static String addPkg(String name)
{
if (name == null)
{
return null;
}
if (pkgroot == null)
{
return name;
}
return pkgroot + "." + name;
}
/**
* Remove the package prefix from the given class name.
*
* @param name
* Class name.
*
* @return The class name with the root package prefix removed.
*/
private static String removePkg(String name)
{
if (name == null)
{
return null;
}
if (pkgroot == null)
{
return name;
}
if (name.startsWith(pkgroot))
{
name = name.substring(pkgroot.length() + 1);
}
return name;
}
/**
* Worker to get the path separator from the legacy environment. Since we can have multiple
* environments configured in multiple sessions, We store this in the WorkAre {@link WorkArea#pathSep}
*
* @return String
* The path separator from the legacy environment.
*/
private static String getLegacyPathSeparator()
{
WorkArea wa = local.get();
if (wa.pathSep == null)
{
wa.pathSep = EnvironmentOps.getLegacyPathSeparator();
}
return wa.pathSep;
}
/**
* Worker to get the case-sensitivity from the legacy environment. Since we can have multiple
* environments configured in multiple sessions, We store this in the WorkAre {@link WorkArea#caseSens}
*
* @return boolean
* The case-sensitivity from the legacy environment.
*/
private static boolean getLegacyCaseSensitive()
{
WorkArea wa = local.get();
if (wa.caseSens == null)
{
wa.caseSens = EnvironmentOps.getLegacyCaseSensitive();
}
return wa.caseSens;
}
/**
* Container for all legacy class information.
*/
private static class LegacyClass
{
/** A map of overloaded methods to their signatures. The key is the legacy method name. */
private Map<String, List<InternalEntry>> overloads = new HashMap<>();
/**
* A map of all overloaded methods, including from super-classes. The key is an intern'ed string, so an
* identity map can be used.
*/
private Map<String, List<InternalEntry>> overrides = new IdentityHashMap<>();
/** A list of overloaded constructors to their signatures. */
private List<InternalEntry> ctors = new LinkedList<>();
/** The static constructor for this legacy type. */
private InternalEntry staticCtor = null;
/** The destructor for this legacy type. */
private InternalEntry dtor = null;
/** The resolved list of the destructors (super-class first). */
private List<InternalEntry> dtors = null;
/** The execute methods for this type and its hierarchy. */
private Method[] executeMethods = null;
/** A map of property getters to their signatures. The key is the legacy property name. */
private Map<String, List<InternalEntry>> getters = new HashMap<>();
/** A map of property setters to their signatures. The key is the legacy property name. */
private Map<String, List<InternalEntry>> setters = new HashMap<>();
/** The map of each property's extent (where it applies). */
private Map<String, Integer> extents = new CaseInsensitiveHashMap<>();
/**
* Get all overloads of this method name; only the overrides of the methods are included (their super-class
* definitions matching name and parameters will not be included).
*
* @param type
* The legacy {@link com.goldencode.p2j.oo.lang._BaseObject_} sub-class.
* @param iename
* The legacy name.
*
* @return list of the overloaded methods, from all super-classes.
*/
public synchronized List<InternalEntry> getOverrides(Class<?> type, String iename)
{
// do not lowercase - the space of possible case-insensitive variations is usually small, and just
// spend some space to save the list for these cases, instead of lowercasing always.
String key = iename.intern();
List<InternalEntry> res = overrides.get(key);
if (res != null)
{
return res;
}
res = new ArrayList<>();
overrides.put(key, res);
LinkedHashSet<Class<?>> types = new LinkedHashSet<>();
types.add(type);
if (type.isInterface())
{
Utils.collectInterfaces(type, types);
}
else
{
Class<?> cls = type;
while (cls != Object.class)
{
types.add(cls);
cls = cls.getSuperclass();
}
}
for (Class<?> cls : types)
{
if (!_BaseObject_.class.isAssignableFrom(cls))
{
continue;
}
List<InternalEntry> l = SourceNameMapper.getOverloads(cls, iename);
if (l != null)
{
for (InternalEntry ie : l)
{
boolean ok = true;
for (InternalEntry oie : res)
{
if (oie.getMethod().getName().equals(ie.getMethod().getName()) &&
Arrays.equals(oie.getMethod().getParameterTypes(), ie.getMethod().getParameterTypes()))
{
ok = false;
break;
}
}
if (ok)
{
res.add(ie);
}
}
}
}
return res;
}
}
/**
* Container for all external program information.
*/
static class ExternalProgram
{
/** Legacy 4GL name for this external program. */
final String pname;
/** The legacy 4GL name for this external program without the extension. */
final String pnameNoExt;
/** The extension of the legacy 4GL name for this external program. */
final String pnameExt;
/** Converted Java name for this external program. */
final String jname;
/** The named events published by this procedure. */
final String publishedEvents;
/** The qualified OO name if this is a legacy class or interface. */
String ooname = null;
/**
* List of all internal entries, as they appear in the name_map file.
* This is needed to provide consistency for the internal-entries
* attribute (as a function and a procedure may have the same name,
* a map can not be used).
*/
Map<InternalEntryKey, InternalEntry> ieMap = new LinkedHashMap<>();
/**
* Provide fast access to the java name of a specified legacy function
* name.
*/
Map<String, InternalEntry> p2jf = Collections.EMPTY_MAP;
/**
* Provide fast access to the java name of a specified legacy procedure
* name.
*/
Map<String, InternalEntry> p2jp = Collections.EMPTY_MAP;
/**
* Provide fast access to the legacy name of a specified java function
* name.
*/
Map<String, InternalEntry> j2pf = Collections.EMPTY_MAP;
/**
* Provide fast access to the legacy name of a specified java procedure
* name.
*/
Map<String, InternalEntry> j2pp = Collections.EMPTY_MAP;
/**
* The <code>internal-entry</code> attribute for this external program,
* without private members.
*/
String internalEntries = null;
/**
* The <code>internal-entry</code> attribute for this external program,
* with private members.
*/
String internalEntriesPrv = null;
/** The internal-entry associated with the MAIN (external) procedure. */
InternalEntry main = null;
/**
* Basic c'tor.
*
* @param pname
* Legacy 4GL name for this external program.
* @param jname
* Converted Java name for this external program.
* @param publishedEvents
* The named events published by this procedure.
*/
ExternalProgram(String pname, String jname, String publishedEvents)
{
this.pname = pname.intern();
this.jname = jname.intern();
this.publishedEvents = (publishedEvents == null ? "" : publishedEvents.intern());
int lastDotIdx = pname.lastIndexOf('.');
pnameNoExt = lastDotIdx > 0 ? pname.substring(0, lastDotIdx) : pname;
pnameExt = lastDotIdx > 0 ? pname.substring(lastDotIdx + 1) : null;
}
/**
* Gets the <code>internal-entry</code> attribute for this external
* program. If this is the first access, it initializes it.
*
* @param prv
* Flag to include or not the private members.
*
* @return See above.
*/
String getInternalEntries(boolean prv)
{
if (prv)
{
if (internalEntriesPrv == null)
{
internalEntriesPrv = buildInternalEntries(this, prv).intern();
}
return internalEntriesPrv;
}
else
{
if (internalEntries == null)
{
internalEntries = buildInternalEntries(this, prv).intern();
}
return internalEntries;
}
}
/**
* Search the {@link #ieMap internal-entry list} for the specified
* name, using the <code>function</code> flag to search only for
* functions or only for procedures.
*
* @param pname
* Legacy 4GL name for the search internal-entry. When <code>null</code>, the
* target is always the external program.
* @param function
* <code>true</code> if search for a function
*
* @return The {@link InternalEntry} instance, or <code>null</code> if
* it was not found.
*/
InternalEntry search(String pname, boolean function)
{
if (pname == null || pname.isEmpty())
{
// this is the external program
return main;
}
InternalEntryKey key = new InternalEntryKey(function, pname);
return ieMap.get(key);
}
/**
* Search the fast-access p2j maps for a function or procedure, using
* the <code>function</code> flag.
*
* @param pname
* Legacy 4GL name for the search internal-entry.
* @param function
* <code>true</code> if search for a function
*
* @return The {@link InternalEntry} instance, or <code>null</code> if
* it was not found.
*/
InternalEntry searchp(String pname, boolean function)
{
Map<String, InternalEntry> p2j = function ? p2jf : p2jp;
return p2j.get(pname.toLowerCase());
}
/**
* Search the fast-access j2p maps for a function or procedure. This
* ignores the IN SUPER and IN HANDLE entries, as it is expected to
* always find a method with its body in this {@link ExternalProgram}.
*
* @param jname
* Java method name.
*
* @return The {@link InternalEntry} instance, or <code>null</code> if
* it was not found.
*/
InternalEntry searchLocalj(String jname)
{
// in this case, we need only internal entries which body resides
// in this procedure. so, IN SUPER and IN HANDLE entries are ignored.
InternalEntry ie = j2pf.get(jname);
if (ie != null && !ie.isInHandle() && !ie.isSuper())
{
return ie;
}
ie = j2pp.get(jname);
if (ie != null && !ie.isInHandle() && !ie.isSuper())
{
return ie;
}
return null;
}
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o == null || getClass() != o.getClass())
{
return false;
}
ExternalProgram that = (ExternalProgram) o;
return pname.equals(that.pname);
}
@Override
public int hashCode()
{
return Objects.hash(pname);
}
}
/**
* Map key for internal entry objects.
*/
private static class InternalEntryKey
{
/** Does entry represent a function? */
private final boolean function;
/** Normalized (lowercased) legacy entry name */
private final String name;
/** The cached hash code. */
private final int hashCode;
/**
* Constructor.
*
* @param function
* Does entry represent a function?
* @param name
* Legacy name.
*/
private InternalEntryKey(boolean function, String name)
{
this.function = function;
this.name = name;
int result = 17;
result = 37 * result + (function ? 5 : 3);
result = 37 * result + StringHelper.hashCodeCaseInsensitive(name);
this.hashCode = result;
}
/**
* Generate a hash code consistent with {@link #equals(Object)}.
*
* @return Hash code.
*/
@Override
public int hashCode()
{
return hashCode;
}
/**
* Determine equivalence with the given key object.
*
* @return {@code true} if the internal states of this key and the given key match,
* else {@code false}.
*/
@Override
public boolean equals(Object o)
{
InternalEntryKey that = (InternalEntryKey) o;
return (this.function == that.function && this.name.equalsIgnoreCase(that.name));
}
}
/**
* A cache class that contains the propath and a map of indexes for search-specs mapped to their
* target procedures (the result obtained from {@link SourceNameMapper#fastConvertName(String)}).
*
* The propath is used for double-checking the cache when it is read.
*/
private static class SourceNameMapperCache
{
/** Propath for which the cache was created */
private String[] propath;
/**
* A map of program indexes mapped to their target procedures.
* The program index is obtained from {@link SourceNameMapper#initializationPaths} and
* is used instead of the search-spec to reduce the number of Java Strings being used.
*/
private LRUCache<Integer, String[]> convertedNames;
/** Lock for read/write operations related to convertedNames. */
private ReentrantReadWriteLock lock;
/**
* Constructor
*
* @param propath
* The propath for which the cache is created.
* @param convertedNames
* Initial map of indexed search-specs mapped to their target procedure.
*/
private SourceNameMapperCache(String[] propath, LRUCache<Integer, String[]> convertedNames)
{
this.propath = propath;
this.convertedNames = convertedNames;
this.lock = new ReentrantReadWriteLock();
}
/**
* Returns the propath of the cache.
*
* @return the propath
*/
private String[] getPropath()
{
return propath;
}
/**
* Check if the given program name has an entry in the converted names map.
*
* @param programIndex
* The index of the program name that is checked.
*
* @return <code>true</code> if the program was previously converted and has
* an entry in the map, <code>false</code> otherwise.
*/
private boolean hasProgram(Integer programIndex)
{
return convertedNames.containsKey(programIndex);
}
/**
* Given an index of a program name, check if it is mapped to any target procedure and
* return it if so.
*
* @param programIndex
* The index of the program name that is searched.
*
* @return The target procedure or <code>null</code> if there is none;
*/
private String[] getResolvedProgram(Integer programIndex)
{
return convertedNames.get(programIndex);
}
/**
* Add a new converted result of a program using the index of the program.
*
* @param programIndex
* The index of the program which is used as key.
* @param result
* The result obtained from fastConvertName of the program.
*/
private void addResolvedProgram(Integer programIndex, String[] result)
{
convertedNames.put(programIndex, result);
}
/**
* Lock read operation.
*/
private void readLock()
{
lock.readLock().lock();
}
/**
* Unlock read operation.
*/
private void readUnlock()
{
lock.readLock().unlock();
}
/**
* Lock write operation.
*/
private void writeLock()
{
lock.writeLock().lock();
}
/**
* Unlock write operation.
*/
private void writeUnlock()
{
lock.writeLock().unlock();
}
}
/**
* A thread factory class that is used to create AssociatedThread(s) for creating source cache entries.
* The threads will have a lower priority.
*/
private static class SourceThreadFactory
implements ThreadFactory
{
@Override
public Thread newThread(Runnable r)
{
Thread workThread = new AssociatedThread(r);
workThread.setName("Source cache entry task");
workThread.setPriority(4);
return workThread;
}
}
/**
* A custom Runnable class that monitors identifiers represented by the propath hash code used
* by the {@link MonitorRunnable#task} to create a source cache entry. This identifier is used
* to avoid queuing source cache entry threads for the same propath.
*/
private static class MonitorRunnable
implements Runnable
{
/** Set of all identifiers for which a source cache entry task was created. */
private static final Set<Long> identifiers = new HashSet<>();
/** The task that needs to be executed to create a new source cache entry. */
private final Runnable task;
/**
* Basic constructor that initializes the class members.
*
* @param identifier
* The propath hash code used by the task.
* @param task
* The task that needs to be executed to create a new source cache entry.
*/
private MonitorRunnable(Long identifier, Runnable task)
{
synchronized (identifiers)
{
identifiers.add(identifier);
}
this.task = task;
}
/**
* Override of the run method for the Runnable that only runs the task provided when instantiating
* the object.
*/
@Override
public void run()
{
task.run();
}
/**
* Method that checks if the identifier provided was previously submitted.
*
* @return {@code true} if there is no identifier submitted, {@code false} otherwise.
*/
private static boolean hasIdentifier(Long identifier)
{
synchronized (identifiers)
{
return identifiers.contains(identifier);
}
}
/**
* Method that clears the identifier of a source cache entry after it is created.
*
* @param identifier
* The identifier for the source cache entry that needs to be removed.
*/
private static void removeIdentifier(Long identifier)
{
synchronized (identifiers)
{
identifiers.remove(identifier);
}
}
}
/**
* This interface is implemented by classes which search for a program name
* representing a Progress procedure file name that can be matched to
* an existent map table of Java classes.
*/
private interface PathLookup
{
/**
* Looks up the given program name based on the propath and the case sensitivity
* values of the WorkArea.
*
* @param legacyProgName
* The program name that is searched.
* @param propath
* The propath of the current context.
* @param caseSens
* The case sensitivity of the current context.
*
* @return The program name which represents a Progress procedure file name
* if there is a match or <code>null</code> if there is no match.
*/
public String lookupPath(String legacyProgName, String[] propath, boolean caseSens);
}
/**
* A lookup class which stores possible paths of a program and can be used to retrieve
* the same ExternalProgram for each of those paths. A possible path is either
* the unqualified program name or any path pre-pended by a propath value.
*/
private static class SinglePathLookup
implements PathLookup
{
/** The full path to the program */
private String programName;
/** The unqualified program name */
private String simpleProgramName;
/**
* Constructor which initializes the program name.
*
* @param programName
* The full path of the program.
*/
public SinglePathLookup(String programName)
{
this.programName = programName;
int fileSepIndex = programName.lastIndexOf(fileSep);
if (fileSepIndex > 0)
{
this.simpleProgramName = programName.substring(fileSepIndex + 1);
}
else
{
this.simpleProgramName = programName;
}
}
/**
* Getter for the simpleProgramName property.
*
* @return The unqualified program name.
*/
public String getSimpleProgramName()
{
return simpleProgramName;
}
/**
* Looks up the given program name based on the propath and the case sensitivity
* values of the WorkArea. It will try to match the program name with the existing
* full path of the instance to return early.
* If there are path aliases in use, then it will try to resolve the program name
* before trying the same after removing any pre-pended propath.
*
* @param legacyProgName
* The program name that is searched.
* @param propath
* The propath of the current context.
* @param caseSens
* The case sensitivity of the current context.
*
* @return The program name which represents a progress procedure file name
* if there is a match or <code>null</code> if there is no match.
*/
public String lookupPath(String legacyProgName, String[] propath, boolean caseSens)
{
String answerProgram = caseSens ? programName : programName.toLowerCase();
if (legacyProgName.equals(answerProgram))
{
return programName;
}
if (pathAliases != null)
{
String[] aliases = resolvePathAliases(legacyProgName);
for (int i = 0; i < aliases.length; i++)
{
if (aliases[i].equals(answerProgram))
{
return programName;
}
}
}
for (int i = 0; i < propath.length; i++)
{
String filterProgram = answerProgram;
String currentPropath = propath[i];
// This should be the case already handled by the short-circuit above
// the only exception is that the empty or "." propath can be found
// much later in the array.
if (currentPropath.isEmpty() || currentPropath.equals("."))
{
continue;
}
if (filterProgram.startsWith(currentPropath))
{
String possiblePath;
int propathLength = currentPropath.length();
if (filterProgram.charAt(propathLength) == fileSepChar)
{
possiblePath = filterProgram.substring(propathLength + 1);
}
else
{
possiblePath = filterProgram.substring(propathLength);
}
if (possiblePath.equals(legacyProgName))
{
return programName;
}
if (pathAliases != null)
{
String[] aliases = resolvePathAliases(possiblePath);
for (int j = 0; j < aliases.length; j++)
{
if (aliases[j].equals(legacyProgName))
{
return programName;
}
}
}
}
}
return null;
}
}
/**
* A lookup class which stores multiple SinglePathLookup instances. When looking up
* a program name, all references are searched for the path and if only ONE match is
* found then it is returned. Multiple answers cause a conflict and in this case
* <code>null</code> is returned.
*/
private static class MultiPathLookup
implements PathLookup
{
/** A list of lookup instances for the same program name */
private List<SinglePathLookup> references;
/**
* Constructor which initializes the list of SinglePathLookup instances.
*/
public MultiPathLookup()
{
references = new ArrayList<>();
}
/**
* Returns a match after looking through the list of SinglePathLookup instances.
* If the is more than one match found, it will short-circuit the lookup and
* return <code>null</code>.
*
* @param legacyProgName
* The program name that is searched.
* @param propath
* The propath of the current context.
* @param caseSens
* The case sensitivity of the current context.
*
* @return The program name which represents a progress procedure file name
* if there is only one match found or <code>null</code> if there are
* multiple matches or none could be found.
*/
public String lookupPath(String legacyProgName, String[] propath, boolean caseSens)
{
String result = null;
for (SinglePathLookup spl : references)
{
String possibleResult = spl.lookupPath(legacyProgName, propath, caseSens);
if (result == null && possibleResult != null)
{
result = possibleResult;
continue;
}
// The result is already set, so any possible result is a conflict and should return null
// in order to fallback to the old implementation
if (possibleResult != null)
{
return null;
}
}
return result;
}
}
/**
* Container for context-local data.
*/
private static class WorkArea
{
/** The current context's PROPATH value (prepared, depending on the OS). */
private String[] propath = null;
/** The current context's PROPATH value (unprepared). */
private String[] originalPropath = null;
/** Path separator used for the Progress file name data. */
private String pathSep = null;
/** Case sensitivity flag for the Progress file name data. */
private Boolean caseSens = null;
/** Windows flag */
private boolean isWindows;
/** Hash value of the propath that is computed when it is set. */
private Long propathHash = null;
}
}