# Configuration.java

    /*
    ** Module   : Configuration.java
    ** Abstract : Master configuration object
    **
    ** Copyright (c) 2004-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ----------------------------------Description-----------------------------------
    ** 001 ECF 20041217   @19157 Created initial version.
    ** 002 ECF 20050120   @19359 Added support for individual parameters with
    **                           variable substitution. New public methods:
    **                           - getParameter (two flavors)
    **                           - putParameter
    ** 003 GES 20050223   @19931 Added a public utility method to normalize
    **                           a passed in filename to the project root so
    **                           that tools can refer to the same file from
    **                           different current directories without having
    **                           to use absolute filenames.
    ** 004 ECF 20050225   @19963 Modified toFile method to canonicalize file.
    ** 005 GES 20050617   @21507 Moved sweep() to StringHelper.
    ** 006 GES 20070411   @32924 Forced a default value of "." for P2J_HOME
    **                           since this is essentially the only value
    **                           we ever use anyway. This is just safer and
    **                           easier than forcing everyone to always
    **                           specify a value.
    ** 007 GES 20070717   @34580 Exposed the ${SYMBOL} style parameter
    **                           substitution as an external service. Any of
    **                           the parameters in the parmMap will be matched
    **                           and replaced as is normally done during
    **                           putParameter(). This allows other config
    **                           files to act like and reference the main
    **                           system config.
    ** 008 GES 20090424   @41927 Import change.
    ** 009 LMR 20110105          Replaced Apache Digester usage with direct SAX
    **                           XML processing using and the tools in
    **                           GenericSAXHandler and Mapper; performed other
    **                           refactoring:
    **                             -renamed load() to getSchemaConfig()
    **                             -removed the 'key' parameter and throws clause
    **                              of getSchemaConfig()
    **                             -inlined code in initialize() in the static
    **                              initializer block and removed initialize()
    **                             -added main() method to test the parsing of
    **                              the XML file received in args[0]
    ** 010 GES 20110712          Added a helper to return back parameter values
    **                           already converted to the proper type.
    ** 011 ECF 20130125          Added schema metadata configuration. Added generics.
    ** 012 SVL 20130624          Use singleton instead of static initialization. Added server run time
    **                           mode.
    ** 013 CA  20131024          The configuration file must be loaded at runtime mode too.
    ** 014 SVL 20140320          Load configuration from resource in runtime mode.
    ** 015 IAS 20160331          Fix the instance field initialization
    ** 015 IAS 20160509          Fixed DDL dialects processing in case of more than one
    **                           default namespace.
    ** 016 OM  20170113          Unify settings for windows and unix/linux in p2j.cfg.xml.
    ** 017 GES 20170531          Added forceHome() method.
    ** 018 CA  20180321          Added support for PROCESS-ARCHITECTURE, arch configuration parameter.
    ** 019 CA  20180511          Added getPathAliases.
    ** 020 CA  20190516          Added normalizeFilename(home, path).
    ** 021 ECF 20190619          Added configuration for include file reference prefix remapping.
    **     GES 20190621          Fixed include file prefix processing to allow projects that don't
    **                           have it configured (previous version caused an NPE in every parse).
    ** 022 IAS 20200617          No fatal error if p2j.cfg.xml is not found.
    **     ECF 20200906          New ORM implementation.
    ** 023 GES 20200720          Calculate the source-charset if missing. Use windows-1252 for opsys == WIN32
    **                           and use the JVM default charset otherwise.
    **     OM  20200902          The "importFile", "xmlFile" attributes of "namespace" became optional.
    **     OM  20210920          Added support for grouping namespaces into profiles and select one from command
    **                           line parameter.
    **     OM  20220214          Improved messages and lowered error level for dynamic schema loading logs.
    **     OM  20220324          Added optional "dataFolder" attribute to a [namespace].
    **     CA  20220402          Added 'getConversionFolder', which reads the 'cvtpath' parameter and defaults to
    **                           'cvt' directory relative to conversion project home.
    **     CA  20220403          Cache the resolved 'cvtpath' as an absolute path.
    **     OM  20220330          Moved database conversion artifacts to ${cvtpath} folder.
    **     CA  20220412          Added file-set support (either -Z command-line option or p2j.cfg.xml
    **                           configuration).
    **     OM  20220421          Restored the ability to load the configuration file relative to P2J_HOME folder.
    **     CA  20220501          Each profile has the same structure as the main 'global' config.  Allow multiple
    **                           profiles to be ran at once, with the conversion switching the state between
    **                           profiles, when a resource (like a file or namespace) is being processed.  Only
    **                           front phase is supported at this time.
    **     ECF 20220516          Updated SAX handler to add SQL conversion override mode for a schema namespace.
    **     CA  20220520          Added other extensions which need to be checked in 'withFileProfile', for
    **                           different conversion phases, like M0/M1.
    **     ECF 20230127          Added mutable schema configuration option.
    ** 024 VVT 20230318          Constant added: default legacy unit test framework type. See #6237.
    ** 025 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 026 ICP 20240705          Fixed method of obtaining the class loader.
    ** 027 GBB 20240709          Making PROP_HOME_DIR public to be reused.
    */

    /*
    ** 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.cfg;

    import java.io.*;
    import java.nio.charset.*;
    import java.nio.file.*;
    import java.util.*;
    import java.util.function.*;
    import javax.xml.parsers.*;

    import com.goldencode.p2j.classloader.MultiClassLoader;
    import com.goldencode.p2j.util.logging.*;
    import org.xml.sax.*;
    import com.goldencode.p2j.schema.*;
    import com.goldencode.p2j.schema.SchemaConfig.*;
    import com.goldencode.p2j.schema.SchemaConfig.NsConfig.*;
    import com.goldencode.p2j.uast.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.util.*;

    /**
     * Master configuration object which loads subsystem configurations. The
     * main configuration and all subsystem configurations are stored as XML
     * documents, which are expected to be found in a <code>cfg</code>
     * subdirectory of the P2J home path. The home path is defined by the
     * required <code>P2J_HOME</code> system property.
     * <p>
     * Subsystem configurations are java objects returned by the {@link
     * #getSchemaConfig()} method. They are created from XML configuration
     * information by a SAX parser using the <code>GenericSAXHandler</code> and
     * <code>Mapper</code> classes in package <code>com.goldencode.p2j.util
     * </code>. XML configuration data is stored in a file named <code>p2j.cfg.xml
     * </code>.
     * <p>
     * This class also provides services to get and store individual parameter
     * entries which are read from the main configuration file upon initialization
     * (from cfg/global/parameter XML elements).
     * <p>Variable substitution is supported in these parameters, in the form
     * <code>${key_name}</code>, where <code>key_name</code> is the name of a
     * parameter previously defined by this mechanism. The replacement value
     * applied to the substitution variable is the value of the parameter
     * identified by <code>key_name</code>. For example,
     * <pre>
     *    &lt;cfg&gt;
     *      &lt;global&gt;
     *         &lt;parameter name="user.home" value="/home/user" /&gt;
     *         &lt;parameter name="subdir"    value="${user.home}/mysubdir" /&gt;
     *      &lt;/global&gt;
     *      ...
     *    &lt;/cfg&gt;
     * </pre>
     * results in the following parameter definitions after substitution:
     * <pre>
     *    user.home = /home/user
     *    subdir    = /home/user/mysubdir
     * </pre>
     * <strong>Note:</strong> the following restrictions apply to this mechanism:
     * <ul>
     * <li><code>P2J_HOME</code> is a reserved parameter key. It may not be
     *     defined in the configuration file, but it is available for use as a
     *     substitution variable.</li>
     * <li>The variable name <code>key_name</code> must not include any
     *     characters which have special syntax meaning in regular expressions.
     *     Regular expressions are used internally for the replacement; special
     *     characters in <code>key_name</code> are not explicitly escaped,
     *     which could cause unexpected results.</li>
     * </ul>
     */
    public final class Configuration
    {
       /** Constant denoting the {@code <cfg>} XML tag. */
       public static final String TAG_CFG = "cfg";

       /** Constant denoting the {@code <global>} XML tag. */
       public static final String TAG_GLOBAL = "global";

       /** Constant denoting the {@code <parameter>} XML tag. */
       public static final String TAG_PARAMETER = "parameter";

       /** Constant denoting the {@code <file-set>} XML tag. */
       public static final String TAG_FILE_SET = "file-set";

       /** Constant denoting the {@code <directory>} XML tag. */
       public static final String TAG_DIRECTORY = "directory";

       /** Constant denoting the {@code <filename>} XML tag. */
       public static final String TAG_FILENAME = "filename";

       /** Constant denoting the {@code <exclude>} XML tag. */
       public static final String TAG_EXCLUDE = "exclude";

       /** Constant denoting the {@code <include>} XML tag. */
       public static final String TAG_INCLUDE = "include";

       /** Constant denoting the {@code include-mappings>} XML tag. */
       public static final String TAG_INCLUDE_MAPPINGS = "include-mappings";

       /** Constant denoting the {@code schema>} XML tag. */
       public static final String TAG_SCHEMA = "schema";

       /** Constant denoting the {@code profile>} XML tag. */
       public static final String TAG_PROFILE = "profile";

       /** Constant denoting the {@code namespace>} XML tag. */
       public static final String TAG_NAMESPACE = "namespace";

       /** Constant denoting the {@code dialect-specific>} XML tag. */
       public static final String TAG_DIALECTSPECIFIC = "dialect-specific";

       /** Constant denoting the {@code metadata>} XML tag. */
       public static final String TAG_METADATA = "metadata";

       /** Constant denoting the {@code table>} XML tag. */
       public static final String TAG_TABLE = "table";

       /** The parameter name for conversion root path. Default value is {@code cvt/}. */
       public static final String TAG_CVTPATH = "cvtpath";

       /** System property name for P2J home path */
       public static final String PROP_HOME_DIR = "P2J_HOME";

       /**
        * System property name for P2J file separator used internally. We have chosen '/', same as
        * for Linux/UNIX, but it could have been any other char as long as it does not interfere with
        * UNIX and Windows file systems.
        */
       public static final char P2J_FILE_SEP = '/';

       /**
        * System property name for P2J path separator used internally. We have chosen ':', same as
        * for Linux/UNIX, but it could have been any other char as long as it does not interfere with
        * UNIX and Windows file systems.
        */
       public static final char P2J_PATH_SEP = ':';

       /** Configuration subdirectory name */
       private static final String CFG_DIR = "cfg";

       /** Configuration file name extension */
       private static final String CFG_EXTENSION = ".cfg.xml";

       /** Default configuration filename */
       public static final String DEF_CFG_FILE = "p2j.cfg.xml";

       /** Default legacy unit test type */
       public static final String DEF_UNIT_TEST_TYPE = "ABLUnit";

       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(Configuration.class);

       /** Singleton instance. */
       private static volatile Configuration instance;

       /** The pristine configuration instance loaded from the file. */
       private static volatile ProfileConfig globalConfig;

       /**
        * If <code>true</code> then it is a server run time mode when configuration is NOT read from
        * the configuration directory.
        */
       private static boolean runtimeConfig = false;

       /**
        * Flags the import mode (configuration is NOT read from the configuration directory) and the server does
        * not run but the schema information is read from the application's jar.
        */
       private static boolean importMode = false;

       /** The {@link #TAG_INCLUDE_MAPPINGS  include remappings}. */
       private final Map<String, String> includeRemap = new LinkedHashMap<>();

       /** Map of parameters */
       private final Map<String, String> parmMap = new LinkedHashMap<>();

       /** The map of defined profiles. */
       private final Map<String, ProfileConfig> profiles = new LinkedHashMap<>();

       /** Factory providing the SAX parser instance that will be used to parse the config file. */
       private SAXParserFactory saxParserFactory;

       /** The parser instance. */
       private SAXParser saxParser;

       /**
        * The generic SAX handler that will be used by the parser after it's
        * configured to work with P2J config files.
        */
       private GenericSAXHandler saxHandler;

       /** The resolved <code>cvtpath</code> parameter, as an absolute path. */
       private String cvtpath = null;

       /** The configured {@link #TAG_FILE_SET file-set(s)}. */
       private Map<String, FileSet> fileSet;

       /** A mapping of each processed file to its profile, from where it was loaded. */
       private Map<String, String> fileProfile;

       /** A mapping of each database to its profile, from where it was loaded. */
       private Map<String, Set<String>> dbProfile;

       /** The set of active profiles, in multi-profile mode. */
       private Set<String> activeProfiles;

       /**
        * The current active profile, in multi-profile mode.  If this is null, {@link #globalConfig} is assumed.
        */
       private ProfileConfig activeProfile = null;

       /** The resolved files from the {@link #fileSet}, if available. */
       private FileList fileList;

       /** The loaded schema configuration. */
       private SchemaConfig schemaConfig = null;

       /**
        * Holds a set of configuration parameter names whose values are OS dependent. These are
        * related to the system we run now, and are invisible for original P4GL code.
        *
        * The values from {@code p2j.cfg.xml} are encoded with internal {@code P2J_FILE_SEP} and
        * {@code P2J_PATH_SEP} separators, but they need to be switched when configuration file is
        * loaded, on the fly, to actual separators for the OS we are running on.
        *
        * We exclude the {@code PROP_HOME_DIR} from this list because it is internally computed and
        * the result uses the correct path separator.
        */
       private static final Set<String> OS_DEPENDENT_VALUES = new HashSet<>();
       static {
          String[] depVal =
          {
             "basepath",                // path used for computing relative paths for procedure lookup
             "callgraph-db-folder",     // the storage directory for DB graph
             "codenames",               // the matching list for source code name conversion file
             "datanames",               // the matching list for database name conversion file
             "dmo_index_merge",         // dmo_index_merge.xml file for merging DMO definitions
             "dynamic-append",          // set of path to dynamic append to [propath]
             "dynamic-prepend",         // set of path to dynamic prepend to [propath]
             "error-dest",              // file used for preprocessor error messages
             "keyword-ignore",          // ignore keywords file (the -k parameter on startup of P4GL)
             "matchlist",               // project file holding the match list parameter keys
             "merge-dmo-root",          // location of generated .hbm files
             "name_map_merge",          // file storing the name map to be merged
             "oo-skeleton-path",        // location for the non-project class/interface 4GL code
             "output-root",             // destination of generated files from current project
             "p2j_rules", "P2J_RULES",  // location of TRPL rules in current file-system
             "patpath",                 // set of paths where configuration files can be found
             "propath",                 // set of paths where source procedure files can be found
             "registry",                // location of the registry within the project
             "rootlist",                // filename for the XML registry
             "warn-dest",               // file used for preprocessor warning messages
          };
          // do not add these:
          //    P2J_HOME - the root of the P2J project, it is automatically configured and added
          //    [all boolean values]
          //    pkgroot        - java package format
          //    merge-point    - FWD directory path
          //    path-separator - legacy path separator, must be kept as original
          //    file-separator - legacy file separator, must be kept as original
          //    date-order     - date order (Eg. DMY or MDY) from legacy machine
          //    windowing-year - windowing year for 2 digits year
          //    opsys          - the legacy OS
          //    winsys         - WINDOW-SYSTEM preprocessor built-in variable
          //    arch           - PROCESS-ARCHITECTURE preprocessor built-in variable
          //    wsoptions      - list of options used by WebSpeed Embedded SpeedScript Preprocessor
          //    wsmode         - WebSpeed's compatibility mode name
          //    marker         - marker character. ('-marker n' P4GL command line parameter)
          //    include-spec   - the shell pattern used to filter the list of files
          //    number-group-sep     - character used as group separator in numbers
          //    number-decimal-sep   - character used as decimal separator in numbers
          //    source-code-version  - OpenEdge compatibility version
          //    graph-node-filter    - the fully qualified name of the callgraph filter class
          Collections.addAll(OS_DEPENDENT_VALUES, depVal);
       }

       /**
        * Private default constructor to prevent creation of instances of this class.
        */
       private Configuration()
       {
       }

       /**
        * Determines whether the configuration is in conversion mode (configuration is read from the
        * configuration directory) or server run time mode.
        *
        * @return <code>true</code> if it is a server run time mode (when configuration is NOT read
        *         from the configuration directory).
        */
       public static boolean isRuntimeConfig()
       {
          return runtimeConfig;
       }

       /**
        * Set whether the configuration should be in conversion mode (configuration is read from the
        * configuration directory) or server run time mode.
        *
        * @param isRuntime
        *        <code>true</code> if it is a server run time mode (when configuration is NOT read
        *        from the configuration directory).
        *
        * @throws IllegalStateException
        *         If the configuration instance was already initialized.
        */
       public static void setRuntimeConfig(boolean isRuntime)
       {
          synchronized (Configuration.class)
          {
             if (instance != null)
             {
                throw new IllegalStateException("Configuration was already created!");
             }
             runtimeConfig = isRuntime;
          }
       }

       /**
        * Determines whether the configuration is in import mode (configuration is NOT read from the configuration
        * directory) and server does not run but the schema information is read from the application's jar.
        *
        * @return  {@code true} if now running in import mode as described above.
        */
       public static boolean isImportMode()
       {
          return importMode;
       }

       /**
        * Entering import mode. This method must be called from the @import.xml@ as soon as possible in order to
        * allow database artifacts to be read from the application jar instead of conversion environment.
        */
       public static void setImportMode()
       {
          Configuration.importMode = true;
       }

       /**
        * Get the folder where conversion artifacts are being stored.
        * <p>
        * This is either a relative folder to the {@link #home()} directory, or an absolute path.
        *
        * @return   The <code>cvtpath</code> parameter from <code>p2j.cfg.xml></code>.
        *
        * @throws   ConfigurationException
        *           If the conversion artifacts folder could not be resolved.
        */
       public static String getConversionFolder()
       throws ConfigurationException
       {
          Configuration cfg = getInstance();

          String cvtpath = cfg.cvtpath;

          if (cvtpath != null)
          {
             return cvtpath;
          }

          cvtpath = getParameter(TAG_CVTPATH, "cvt");
          File f = new File(cvtpath);
          if (!f.isAbsolute())
          {
             cvtpath = home() + File.separator + cvtpath;
             f = new File(cvtpath);
          }

          if (!f.exists())
          {
             if (!f.mkdirs())
             {
                throw new ConfigurationException("Path " + f.getAbsolutePath() + " could not be created.");
             }
          }

          if (!f.isDirectory())
          {
             throw new ConfigurationException("Path " + f.getAbsolutePath() + " must be a directory.");
          }

          cfg.cvtpath = f.getAbsolutePath();

          return cvtpath;
       }

       /**
        * Return a schema configuration object. The configuration data
        * itself is in the default XML configuration file <code>p2j.cfg.xml</code>
        * that was parsed when this class was loaded.
        *
        * @return  The configuration object loaded from XML.
        */
       public static SchemaConfig getSchemaConfig()
       {
          return getInstance().schemaConfig;
       }

       /**
        * Resolve the configured {@link #fileSet}.
        *
        * @return   The files used for conversion.
        */
       public static FileList getFileList()
       {
          Configuration cfg = getInstance();

          if (cfg.activeProfiles == null)
          {
             List<String> defaultProfiles = new ArrayList<>();
             for (ProfileConfig o : cfg.profiles.values())
             {
                ProfileConfig profile = (ProfileConfig) o;

                if (profile.isDefault())
                {
                   defaultProfiles.add(profile.getName());
                }
             }

             // if a default profile is set, load it.  it still can be overridden at the command line
             // don't use the 'setDefaultProfile' Profile APIs, as the 'config' instance is still being built.
             cfg.loadConfigProfilesImpl(defaultProfiles);
             // schema will have the default profile set if profile switching occurs
          }

          if (cfg.fileSet == null)
          {
             return null;
          }

          if (cfg.fileList == null)
          {
             try
             {
                boolean caseSens = isCaseSensitive();
                ExplicitFileList res = new ExplicitFileList(caseSens);

                for (Map.Entry<String, FileSet> entry : cfg.fileSet.entrySet())
                {
                   String profile = entry.getKey();
                   FileSet fileSet = entry.getValue();

                   if (fileSet == null)
                   {
                      LOG.warning( "No file-set set for profile " + profile);
                      continue;
                   }

                   FileList fl = processFileSet(fileSet, caseSens);
                   if (fl == null)
                   {
                      return null;
                   }

                   res.addAll(fl.listFilenames());
                }

                cfg.fileList = res;
             }
             catch (IOException e)
             {
                LOG.severe( "Getting file list: ", e);
                return null;
             }
          }

          return cfg.fileList;
       }

       /**
        * Get the configured path aliases.
        *
        * @return   A map with the folder alias to its source folder.
        */
       public static Map<String, String> getPathAliases()
       {
          Map<String, String> pathAliases = null;
          String spaths = Configuration.getParameter("path-aliases");
          if (spaths != null && !spaths.trim().isEmpty())
          {
             spaths = spaths.trim();
             pathAliases = new HashMap<>();

             String[] scpaths = spaths.split(";");
             for (String s : scpaths)
             {
                String[] scpath = s.trim().split("=");
                pathAliases.put(scpath[0], scpath[1]);
             }
          }

          return pathAliases;
       }

       /**
        * Get the custom mappings of include file prefixes to the values with which they should be
        * replaced, when resolving the name of an include file found in 4GL source code into a source
        * file on disk in the conversion project.
        *
        * @return  Include file prefix re-mappings. This map may be empty, but it will not be {@code
        *          null}. Remappings are iterated in the order in which they were defined.
        */
       public static Map<String, String> getIncludeFilePrefixMap()
       {
          return getInstance().includeRemap;
       }

       /**
        * Get the configuration parameter with the specified key. This may have
        * been set in the main configuration file, or added later using the
        * {@link #putParameter} method.
        *
        * @param   key
        *          Name of parameter to retrieve.
        *
        * @return  The value of the specified parameter, or <code>null</code> if
        *          <code>key</code> is an invalid identifier.
        */
       public static String getParameter(String key)
       {
          return getParameter(key, null);
       }

       /**
        * Get the configuration parameter with the specified key. This may have been set in the main
        * configuration file, or added later using the {@link #putParameter} method. Values for
        * parameters known to be OS filesystem dependent (files, paths and their lists) will be
        * returned already converted to current OS.
        *
        * @param   key
        *          Name of parameter to retrieve.
        * @param   defaultValue
        *          Value to be returned if {@code key} cannot be found in the map of parameters.
        *          May be {@code null}.
        *
        * @return  The value of the specified parameter, or {@code defaultValue} if {@code key} is not
        *          found.
        */
       public static String getParameter(String key, String defaultValue)
       {
          String value = getInstance().parmMap.get(key);

          return (value != null ? value : defaultValue);
       }

       /**
        * Get the configuration parameter with the specified key as a boolean value. This may have
        * been set in the main configuration file, or added later using the {@link #putParameter}
        * method.  Values for parameters known to be OS filesystem dependent (files, paths and their
        * lists) will be returned already converted to current OS.
        *
        * @param   key
        *          Name of parameter to retrieve.
        * @param   defaultValue
        *          Value to be returned if {@code key} cannot be found in the map of parameters.
        *
        * @return  The value of the specified parameter, or {@code defaultValue} if {@code key} is not
        *          found.
        */
       public static boolean getParameter(String key, boolean defaultValue)
       {
          String value = getInstance().parmMap.get(key);

          return (value != null ? "true".equals(value) : defaultValue);
       }

       /**
        * Get a state saved at the {@link #activeProfile active profile}, for the specified key.
        * @param    key
        *           The state's key.
        *
        * @return   The saved sate.
        */
       public Object getProfileState(Object key)
       {
          return activeProfile == null ? globalConfig.getState(key) : activeProfile.getState(key);
       }

       /**
        * Save a state at the {@link #activeProfile}.
        *
        * @param    key
        *           The state's key.
        * @param    state
        *           The state to save.
        */
       public void setProfileState(Object key, Object state)
       {
          if (activeProfile == null)
          {
             globalConfig.addState(key, state);
          }
          else
          {
             activeProfile.addState(key, state);
          }
       }

       /**
        * Load all configuration profiles.
        * <p>
        * If the profiles are empty or <code>null</code>, nothing is done.
        * <p>
        * If it contains only one profile, then {@link #loadProfile(String)} is used for single-profile mode.
        * <p>
        * Otherwise, multi-profile mode is activated.
        *
        * @param    profiles
        *           The profile list.
        */
       public static void loadConfigProfiles(List<String> profiles)
       {
          if (profiles == null || profiles.isEmpty())
          {
             return;
          }

          Configuration config = getInstance();
          config.loadConfigProfilesImpl(profiles);
       }

       /**
        * Get the active profile name.
        *
        * @return   See above.
        */
       public String getActiveProfile()
       {
          return activeProfile == null ? null : activeProfile.getName();
       }

       /**
        * Check if we are in {@link #activeProfiles multi-profile} mode.
        *
        * @return   <code>true</code> if {@link #activeProfiles} is set.
        */
       public boolean isMultiProfile()
       {
          return activeProfiles != null;
       }

       /**
        * Switch the profile to the one associated with the specified database namespace.  If <code>null</code>
        * or no profile exists for this namespace, then the {@link #globalConfig} is activated.
        *
        * @param    db
        *           The database namespace.
        * @param    code
        *           The code to run for each profile loading this namespace.
        */
       public void withDbProfile(String db, Consumer<String> code)
       {
          if (activeProfiles == null)
          {
             return;
          }

          Set<String> profiles = dbProfile.get(db.toLowerCase());
          if (profiles == null || profiles.isEmpty())
          {
             return;
          }

          profiles.forEach(profile ->
          {
             setDefaultProfile(profile); // may be null, uses global config

             code.accept(profile);
          });

       }

       /**
        * Switch the profile to the one associated with the specified file.  If <code>null</code>
        * or no profile exists for this file, then the {@link #globalConfig} is activated.
        *
        * @param    file
        *           The file name.
        */
       public void withFileProfile(String file)
       {
          if (activeProfiles == null)
          {
             return;
          }

          String[] exts = { ".ast", ".jast", ".schema", ".dict", ".p2o" };
          for (String ext : exts)
          {
             if (file.endsWith(ext))
             {
                file = file.substring(0, file.length() - ext.length());
                break;
             }
          }

          file = normalizeFilename(file);

          String profile = fileProfile.get(file);
          if (profile == null)
          {
             // look in the file-set from all profiles
             for (String p : activeProfiles)
             {
                ProfileConfig cfg = profiles.get(p);
                if (cfg.getFileSet() == null)
                {
                   continue;
                }

                Set<String> fl = cfg.getFileList();
                if (fl == null)
                {
                   try
                   {
                      FileList fileList = processFileSet(cfg.getFileSet(), importMode);
                      if (fileList == null)
                      {
                         continue;
                      }

                      fl = new HashSet<>();
                      for (String fn : fileList.listFilenames())
                      {
                         fn = normalizeFilename(fn);
                         fl.add(fn);
                      }
                   }
                   catch (IOException e)
                   {
                      LOG.info("", e);
                      // ignore
                      continue;
                   }

                   cfg.setFileList(fl);
                }

                if (fl.contains(file))
                {
                   profile = p;
                }
             }

             if (profile == null)
             {
                System.out.println("WARNING: could not find file '" + file +
                                   "' in profile FILE-SETs - falling back to PROPATH.");
                // look in all profiles PROPATH
                for (String p : activeProfiles)
                {
                   ProfileConfig cfg = profiles.get(p);

                   String[] paths = getPaths(cfg);

                   for (String path : paths)
                   {
                      if (".".equals(path))
                      {
                         continue;
                      }

                      if (file.startsWith(path))
                      {
                         profile = p;
                         break;
                      }
                   }
                }
             }

             fileProfile.put(file, profile);
          }
          setDefaultProfile(profile); // may be null, uses global config
       }

       /**
        * Resolve the profile's proPaths.
        *
        * @param    p
        *           The profile.
        *
        * @return   The paths, relative to P2J_HOME.
        */
       private String[] getPaths(ProfileConfig p)
       {
          String[] paths = p.getPaths();

          if (paths == null)
          {
             setDefaultProfile(null);
             loadProfile(p.getName());

             // load all the PROPATH files for this profile
             String propath = parmMap.get("propath");
             paths = AstGenerator.getPropath(propath, null);
             for (int i = 0; i < paths.length; i++)
             {
                paths[i] = normalizeFilename(paths[i]);
             }

             p.setPaths(paths);
          }

          return paths;
       }

       /**
        * Load all configuration profiles.
        * <p>
        * If the profiles are empty or <code>null</code>, nothing is done.
        * <p>
        * If it contains only one profile, then {@link #loadProfile(String)} is used for single-profile mode.
        * <p>
        * Otherwise, multi-profile mode is activated.
        *
        * @param    profiles
        *           The profile list.
        */
       private void loadConfigProfilesImpl(List<String> profiles)
       {
          if (profiles.isEmpty() || activeProfiles != null)
          {
             return;
          }

          if (profiles.size() == 1)
          {
             loadProfile(profiles.get(0));
             return;
          }

          schemaConfig.deactiveDefaults();

          activeProfiles = new LinkedHashSet<>();
          dbProfile = new HashMap<>();
          fileProfile = new HashMap<>();
          fileSet = new HashMap<>();

          for (String profile : profiles)
          {
             ProfileConfig cfg = this.profiles.get(profile);
             if (cfg == null)
             {
                throw new RuntimeException("Profile " + profile + " does not exist!");
             }
             fileSet.put(profile, cfg.getFileSet());

             activeProfiles.add(profile);
             Set<String> namespaces = schemaConfig.setActive(profile);
             for (String ns : namespaces)
             {
                if ("standard".equals(ns))
                {
                   continue;
                }

                String dictFile = schemaConfig.getSchemaFileName(profile, ns, ".dict");
                dictFile = normalizeFilename(dictFile);
                fileProfile.put(dictFile, profile);
                dbProfile.computeIfAbsent(ns.toLowerCase(), (k) -> new HashSet<>()).add(profile);
             }
          }
       }

       /**
        * Associate a configuration parameter value with {@code key}. Variable substitution is
        * performed on {@code value} before it is added to the map.
        * <p>
        * In the case of OS dependent values, they are adjusted to match the current OS (operation
        * only needed for Windows OS. For Linux/Unix this operation is a no-op because the neutral
        * separators were chosen to match the separators for them).
        *
        * @param   key
        *          A name which uniquely identifies the parameter.
        * @param   value
        *          The parameter's value.
        *
        * @return  The text of {@code value} after variable substitution has been performed.
        *
        * @throws  ConfigurationException
        *          if {@code key} already identifies a configuration parameter.
        */
       private String putParameter(String key, String value)
       throws ConfigurationException
       {
          if (key == null || value == null)
          {
             throw new ConfigurationException("Null parameter key or value");
          }

          if (parmMap.containsKey(key))
          {
             throw new ConfigurationException("Duplicate parameter: " + key);
          }

          // for parameters known to be OS dependent, switch from (neutral) to current OS separators
          if (OS_DEPENDENT_VALUES.contains(key))
          {
             value = value.replace(P2J_FILE_SEP, File.separatorChar);
             value = value.replace(P2J_PATH_SEP, File.pathSeparatorChar);
          }

          // handle variable substitution
          value = substituteParms(value);

          parmMap.put(key, value);

          return value;
       }

       /**
        * Computes missing derived parameter values based on previously set ones.
        */
       private void computeDerivedValues()
       {
          // default is Unix OS, if not set, add it now
          String value = parmMap.computeIfAbsent("opsys", x -> "UNIX");
          boolean isWin = value.equalsIgnoreCase("WIN32");

          // compute defaults for derived parameter values
          parmMap.putIfAbsent("file-separator", isWin ? "\\" : String.valueOf(P2J_FILE_SEP));
          parmMap.putIfAbsent("path-separator", isWin ? ";" : String.valueOf(P2J_PATH_SEP));
          parmMap.putIfAbsent("case-sensitive", String.valueOf(!isWin));
          parmMap.putIfAbsent("unix-escapes",   String.valueOf(!isWin));
          parmMap.putIfAbsent("winsys",         (isWin ? "MS-WIN95" : "TTY"));
          parmMap.putIfAbsent("source-charset", (isWin ? "windows-1252" : Charset.defaultCharset().name()));

          // mandatory default values
          parmMap.putIfAbsent(TAG_CVTPATH, "cvt");
       }

       /**
        * Load the specified profile.  This will use the {@link #globalConfig} to create the parameter mapping
        * (the profile parameters overwrite the ones from the global instance).
        * <p>
        * The {@link #includeRemap} and the {@link #fileSet} will be copied from the specified profile.
        * <p>
        * If we are in {@link #isMultiProfile() multi-profile} mode, and the profile is the same as the
        * {@link #activeProfile}, then this will do nothing, as the profile is already loaded.
        *
        * @param    name
        *           The profile name.
        *
        * @throws   IllegalArgumentException
        *           If the profile name is not registered at {@link #profiles}.
        */
       private void loadProfile(String name)
       {
          if ((name == null && (activeProfile == null || activeProfile == globalConfig)) ||
              (name != null && activeProfile != null && name.equals(activeProfile.getName())))
          {
             // already loaded
             return;
          }

          ProfileConfig profile = name == null ? globalConfig : profiles.get(name.toLowerCase());

          if (profile == null)
          {
             throw new IllegalArgumentException("Profile " + name + " does not exist!");
          }

          // merge the profile into this configuration, using the pristine global instance as the template

          // overwrite parameters, inheriting from global config
          this.parmMap.clear();
          this.parmMap.putAll(globalConfig.getParameters());
          for (Map.Entry<String, String> entry : profile.getParameters().entrySet())
          {
             this.parmMap.remove(entry.getKey());

             try
             {
                putParameter(entry.getKey(), entry.getValue());
             }
             catch (ConfigurationException e)
             {
                throw new RuntimeException(e);
             }
          }

          // replace include remap
          this.includeRemap.clear();
          this.includeRemap.putAll(profile.getIncludeRemap());

          // replace file-set
          this.fileSet = null;
          if (profile.getFileSet() != null)
          {
             this.fileSet = new HashMap<>();
             this.fileSet.put(profile.getName(), profile.getFileSet());
          }
          this.fileList = null;

          // clear the cvtPath
          this.cvtpath = null;
       }

       /**
        * Variable substitution is performed on <code>value</code> before it is
        * returned.  Any portions of the string matching a ${SYMBOL} syntax will
        * have the parameter map searched for SYMBOL. If it is found, the value
        * of that parameter will be inserted in place of ${SYMBOL}.
        * <p>
        * Replacements occur in the same order they were defined in the
        * main configuration file, and then in the order in which they were
        * added by external callers of this method.
        *
        * @param   value
        *          The value on which to operate.
        *
        * @return  The text of <code>value</code> after variable substitution
        *          has been performed.
        */
       public String substituteParms(String value)
       {
          return StringHelper.sweep(value, parmMap);
       }

       /**
        * Get the P2J home path defined for the application or "." if no such
        * path is defined.
        *
        * @return   P2J home path or "." if the system property has not been
        *           defined.
        *
        * @throws   ConfigurationException
        *           If there is any problem with normalizing the specified path.
        */
       public static String home()
       throws ConfigurationException
       {
          // Check for the P2J_HOME system property.
          String home = System.getProperty(PROP_HOME_DIR, ".");

          // Normalize and verify the path.
          File dir = new File(home);
          home = normalizePath(dir, true, true);

          return home;
       }

       /**
        * Given a filename that assumes it is relative to the current directory, check if the
        * project home directory is not "." and if so prepend the filename with the project
        * home value. This is useful to run code when the current directory is not the same
        * as the project root, but project root resources still need to be found.
        *
        * @param    filename
        *           Filename that is relative to the project root.
        *
        * @return   The coerced filename or the original filename if there is no need to
        *           coerce (or on error).
        */
       public static String forceHome(String filename)
       {
          try
          {
             String home = home();
             return ".".equals(home) || filename.startsWith(home)
                       ? filename
                       : home + P2J_FILE_SEP + filename;
          }
          catch (ConfigurationException ce)
          {
             return filename;
          }
       }

       /**
        * If <code>path</code> represents a relative path, generate a
        * <code>File</code> object for this path, relative to the home directory
        * defined for the application. If <code>filename</code> is an absolute
        * path, simply use it to construct the <code>File</code>.
        *
        * @param    path
        *           A relative or absolute file or directory path.
        *
        * @return   The file representing the given path
        *
        * @throws   ConfigurationException
        *           if <code>path</code> is relative, but no home path has been
        *           defined for the application.
        */
       public static File toFile(String path)
       throws ConfigurationException
       {
          File file = new File(path);
          if (!importMode && !file.isAbsolute())
          {
             file = new File(home(), path);
             try
             {
                file = file.getCanonicalFile();
             }
             catch (IOException exc)
             {
                // ignore me, keep the non-canonical value
             }
          }

          return file;
       }

       /**
        * Converts an absolute or relative filename (relative to the current
        * directory) into a filename that is relative to the project home. This
        * allows tools referencing the same file from different current
        * directories to reference the same file, even though the pathing may
        * not appear to be an exact match.
        *
        * @param    filename
        *           Absolute or current directory relative filename.
        *
        * @return   Filename normalized to the project root OR the original
        *           filename if the file exists outside of the project directory
        *           tree (and thus cannot be normalized).
        */
       public static String normalizeFilename(String filename)
       {
          String home;

          try
          {
             home = home();
          }
          catch (ConfigurationException cfg)
          {
             // we are done, there is no normalization to be done if we can't find the project root
             return filename;
          }

          return normalizeFilename(home, filename);
       }

       /**
        * Converts an absolute or relative filename (relative to the current
        * directory) into a filename that is relative to the specified home. This
        * allows tools referencing the same file from different current
        * directories to reference the same file, even though the pathing may
        * not appear to be an exact match.
        *
        * @param    home
        *           The home folder to use.
        * @param    filename
        *           Absolute or current directory relative filename.
        *
        * @return   Filename normalized to the project root OR the original
        *           filename if the file exists outside of the project directory
        *           tree (and thus cannot be normalized).
        */
       public static String normalizeFilename(String home, String filename)
       {
          File   abnormal = new File(filename);
          String absolute;

          try
          {
             // make best effort to clean up the path
             absolute = abnormal.getCanonicalPath();
          }
          catch (IOException exc)
          {
             // fallback plan if canonical doesn't work
             absolute = abnormal.getAbsolutePath();
          }

          try
          {
             home = new File(home).getCanonicalPath();
          }
          catch (IOException exc)
          {
             // ignore
          }

          // make sure this filename is in the project directory tree
          if (absolute.startsWith(home))
          {
             // all is well, we can normalize this path
             return "." + absolute.substring(home.length());
          }

          // the path can't be normalized (because it exists outside the
          // project directory tree, so we can only return the original filename
          return filename;
       }

       /**
        * Get the singleton instance of this class.
        *
        * @return  singleton instance of this class.
        */
       public static Configuration getInstance()
       {
          if (instance == null)
          {
             synchronized (Configuration.class)
             {
                if (instance == null)
                {
                   instance = createConfiguration();
                }
             }
          }
          return instance;
       }

       /**
        * Check if the legacy operating system, from which the source code originates, is case-sensitive.
        *
        * @return   See above.
        */
       public static boolean isCaseSensitive()
       {
          return getParameter("case-sensitive", false);
       }

       /**
        * Set the default profile.
        *
        * @param    name
        *           The profile name.
        */
       public static void setDefaultProfile(String name)
       {
          Configuration config = getInstance();

          if ((name == null && (config.activeProfile == null || config.activeProfile == globalConfig)) ||
              (name != null && config.activeProfile != null && name.equals(config.activeProfile.getName())))
          {
             // already loaded
             return;
          }

          config.schemaConfig.setDefaultProfile(name);
          config.loadProfile(name);

          config.activeProfile = config.profiles.get(name);
       }

       /**
        * Set the default profiles.
        *
        * @param    profiles
        *           The profiles to be loaded.
        */
       public static void setDefaultProfiles(List<String> profiles)
       {
          if (profiles == null || profiles.isEmpty())
          {
             return;
          }

          if (profiles.size() == 1)
          {
             String profile = profiles.get(0);
             setDefaultProfile(profile);
             return;
          }

          loadConfigProfiles(profiles);
          // schema will have the default profile set if profile switching occurs
       }

       /**
        * Create a Configuration instance.
        *
        * @return Configuration instance
        */
       private static Configuration createConfiguration()
       {
          Configuration config = new Configuration();

          try
          {
             // always start with P2J_HOME parameter; this is reserved
             config.parmMap.put(PROP_HOME_DIR, home());

             InputStream cfgStream;

             // attempt to load from jar first
             ClassLoader cl = MultiClassLoader.getClassLoader();
             cfgStream = cl.getResourceAsStream(CFG_DIR + "/" + DEF_CFG_FILE);

             if (cfgStream == null)
             {
                // if the configuration file was not found in jar, try locating it as normal file
                File cfgFile = getConfigFile(getConfigDirectory(), DEF_CFG_FILE);
                if (!cfgFile.exists())
                {
                   return config;
                }

                cfgStream = Files.newInputStream(cfgFile.toPath());
             }

             config.saxParserFactory = SAXParserFactory.newInstance();
             config.saxParser = config.saxParserFactory.newSAXParser();
             config.saxHandler = new GenericSAXHandler();

             // Prepare the generic handler.
             setupHandler(config.saxHandler);

             // Parse the file.
             config.loadXml(cfgStream);

             // map parameters by their names, which should be unique
             List<?> params = config.saxHandler.getGlobalObjects(TAG_PARAMETER);
             for (Object o : params)
             {
                Parameter p = (Parameter) o;
                config.putParameter(p.getName(), p.getValue());
             }

             // if needed, compute derived values
             config.computeDerivedValues();

             if (!isRuntimeConfig())
             {
                List l = config.saxHandler.getGlobalObjects(TAG_FILE_SET);
                if (l == null || l.isEmpty())
                {
                   config.fileSet = null;
                }
                else if (l.size() > 1)
                {
                   throw new ConfigurationException("Only a FILE-SET must be specified!");
                }
                else
                {
                   config.fileSet = new HashMap<>();
                   config.fileSet.put(null, (FileSet) l.get(0));
                }

                // load the include-remap

                List<?> includeRemap = config.saxHandler.getGlobalObjects(TAG_INCLUDE_MAPPINGS);

                if (!includeRemap.isEmpty())
                {
                   IncludeRemap remap = (IncludeRemap) includeRemap.get(0);

                   config.includeRemap.putAll(remap.getMap());
                }
             }

             config.schemaConfig = (SchemaConfig) config.saxHandler.getGlobalObjects(TAG_SCHEMA).get(0);

             // set PK name to schema config
             String pkName = config.parmMap.get("primary-key-name");
             if (pkName != null)
             {
                config.schemaConfig.setPrimaryKeyName(pkName);
             }

             // set the profile map
             List<Object> profiles = config.saxHandler.getGlobalObjects(TAG_PROFILE);
             for (Object o : profiles)
             {
                ProfileConfig profile = (ProfileConfig) o;
                config.profiles.put(profile.getName().toLowerCase(), profile);
                config.schemaConfig.addProfile(profile);
             }

             // save this pristine configuration as the global configuration
             globalConfig = new ProfileConfig();
             globalConfig.setFileSet(config.fileSet == null ? null : config.fileSet.get(0));
             globalConfig.addParameters(config.parmMap);
             globalConfig.setSchema(config.schemaConfig);
             globalConfig.addIncludeRemap(config.includeRemap);
             globalConfig.setName(null);
          }
          catch (Exception exc)
          {
             throw new RuntimeException(new ConfigurationException(
                   "Error loading " + DEF_CFG_FILE + " configuration", exc));
          }
          return config;
       }

       /**
        * Get the configuration directory, which must be a <code>cfg</code>
        * subdirectory beneath the P2J home path.
        *
        * @return  Configuration subdirectory.
        *
        * @throws  ConfigurationException
        *          if configuration directory does not exist or is not a
        *          directory.
        */
       private static File getConfigDirectory()
       throws ConfigurationException
       {
          File dir = new File(home(), CFG_DIR);
          normalizePath(dir, true, true);

          return dir;
       }

       /**
        * Get the configuration file for the subsystem associated with
        * <code>root</code>, which is either <code>&lt;root&gt;.cfg.xml</code>
        * or, if this file is missing, is the default configuration file,
        * <code>p2j.cfg.xml</code>.
        *
        * @param   dir
        *          Configuration directory.
        * @param   root
        *          Name associated with the target subsystem.
        *
        * @return  Configuration file.
        *
        * @throws  ConfigurationException
        *          if configuration file cannot be found.
        */
       private static File getConfigFile(File dir, String root)
       throws ConfigurationException
       {
          File file = new File(dir, root + CFG_EXTENSION);

          if (!file.exists())
          {
             file = new File(dir, DEF_CFG_FILE);
          }

          if (!file.exists())
          {
             throw new ConfigurationException("No data file found for " + root + " configuration");
          }

          return file;
       }

       /**
        * Normalize and optionally verify the specified file system object.
        * <p>
        * Normalizing the path converts relative paths to absolute paths and attempts to remove any
        * redundant, relative constructs, such as references to the current directory ({@code .}) and
        * to the parent directory ({@code ..}).
        * <p>
        * Verifying the path checks that the object exists in the file system and that it is the
        * expected type (file vs. directory).
        *
        * @param    file
        *           File whose path is to be manipulated.
        * @param    verify
        *           {@code true} to verify the path, else {@code false}.
        * @param    isDirectory
        *           {@code true} if {@code file} is supposed to represent a directory, {@code false}
        *           if it should represent a file. This argument is only relevant if {@code verify} is
        *           set to {@code true}.
        *
        * @return   The normalized path
        *
        * @throws   ConfigurationException
        *           if verification fails.
        */
       private static String normalizePath(File file, boolean verify, boolean isDirectory)
       throws ConfigurationException
       {
          String path;

          try
          {
             path = file.getCanonicalPath();
          }
          catch (IOException exc)
          {
             path = file.getAbsolutePath();
          }

          if (verify)
          {
             if (!file.exists())
             {
                throw new ConfigurationException("'" + path + "' does not exist");
             }

             if (file.isDirectory() != isDirectory)
             {
                String type = (isDirectory ? "directory" : "file");
                throw new ConfigurationException("'" + path + "' is not a " + type);
             }
          }

          return path;
       }

       /**
        * Resolve the file set, by using the initial files (loaded from the reference directory or explicit file
        * names), and applying the Include and eXclude operations.
        *
        * @param    fileSet
        *           The file set.
        * @param    caseSens
        *           Flag indicating if the legacy operating system is case-sensitive or not.
        *
        * @return   The resolved file list.
        *
        * @throws   IOException
        *           In case of problems reading files from disk.
        */
       private static FileList processFileSet(FileSet fileSet, boolean caseSens)
       throws IOException
       {
          if (fileSet.getFilename() != null)
          {
             if (fileSet.getOperations().length > 0 || fileSet.getReferences().length > 0)
             {
                LOG.warning( "File references or operations can't be specified if the file-set is " +
                                "loaded from a file.");
                return null;
             }

             // it is loaded from disk, everything else is ignored
             fileSet = FileListFactory.loadFileSet(fileSet.getFilename());
             if (fileSet == null)
             {
                // error encountered
                return null;
             }
          }

          FileList fl = FileListFactory.processFileSet(fileSet, caseSens);
          return fl;
       }

       /**
        * Prepare the general purpose SAX handler for working with P2J XML config
        * file syntax.
        *
        * @param saxHandler
        *        The SAX handler to be configured.
        */
       private static void setupHandler(GenericSAXHandler saxHandler)
       {
          // XML/object mappers:

          saxHandler.installMapper(TAG_PARAMETER, new Mapper<>(Parameter.class));

          saxHandler.installMapper(TAG_INCLUDE_MAPPINGS, new Mapper<IncludeRemap>(IncludeRemap.class)
             {
                @Override
                public void mergeSubObj(IncludeRemap ir, Object c)
                {
                   checkSubObjType(c, Parameter.class);
                   Parameter p = (Parameter) c;
                   ir.remap(p.getName(), p.getValue());
                }
             });

          saxHandler.installMapper(TAG_SCHEMA, new Mapper<SchemaConfig>(SchemaConfig.class)
             {
                @Override
                public void mergeSubObj(SchemaConfig s, Object c)
                {
                   checkSubObjTypes(c, new Class<?>[] { NsConfig.class, Metadata.class });
                   if (c instanceof NsConfig)
                   {
                      s.addNamespace((NsConfig) c);
                   }
                   else
                   {
                      s.setMetadata((Metadata) c);
                   }
                }
             });

          saxHandler.installMapper(TAG_DIRECTORY, new Mapper<>(DirectoryReference.class));
          saxHandler.installMapper(TAG_FILENAME, new Mapper<>(FileReference.class));
          saxHandler.installMapper(TAG_INCLUDE, new Mapper<>(IncludeFilter.class));
          saxHandler.installMapper(TAG_EXCLUDE, new Mapper<>(ExcludeFilter.class));
          saxHandler.installMapper(TAG_FILE_SET, new Mapper<FileSet>(FileSet.class)
          {
             @Override
             protected void mergeSubObj(FileSet p, Object c)
             {
                checkSubObjTypes(c, new Class<?> []
                {
                   DirectoryReference.class,
                   FileReference.class,
                   IncludeFilter.class,
                   ExcludeFilter.class
                });

                if (c instanceof DirectoryReference)
                {
                   p.addReference((DirectoryReference) c);
                }
                else if (c instanceof FileReference)
                {
                   p.addReference((FileReference) c);
                }
                else if (c instanceof IncludeFilter)
                {
                   p.addOperation((IncludeFilter) c);
                }
                else if (c instanceof ExcludeFilter)
                {
                   p.addOperation((ExcludeFilter) c);
                }
             }
          });

          saxHandler.installMapper(TAG_PROFILE, new Mapper<ProfileConfig>(ProfileConfig.class)
             {
                @Override
                public void mergeSubObj(ProfileConfig p, Object c)
                {
                   checkSubObjTypes(c, new Class[]
                   {
                      Parameter.class,
                      FileSet.class,
                      SchemaConfig.class,
                      IncludeRemap.class
                   });

                   try
                   {
                      if (c instanceof Parameter)
                      {
                         Parameter param = (Parameter) c;
                         p.addParameter(param.getName(), param.getValue());
                      }
                      else if (c instanceof FileSet)
                      {
                         p.setFileSet((FileSet) c);
                      }
                      else if (c instanceof SchemaConfig)
                      {
                         p.setSchema((SchemaConfig) c);
                      }
                      else
                      {
                         IncludeRemap r = (IncludeRemap) c;
                         p.addIncludeRemap(r.getMap());
                      }
                   }
                   catch (ConfigurationException ex)
                   {
                      throw new RuntimeException("Error loading profile " + p.getName(), ex);
                   }
                }
             });

          saxHandler.installMapper(TAG_NAMESPACE, new Mapper<NsConfig>(NsConfig.class)
             {
                @Override
                public void mergeSubObj(NsConfig ns, Object c)
                {
                   checkSubObjTypes(c, new Class<?>[] { Parameter.class, DialectParam.class } );
                   if (c instanceof Parameter)
                   {
                      ns.addParameter((Parameter) c);
                   }
                   else
                   {
                      ns.addDialectParameter((DialectParam) c);
                   }
                }
             });

          saxHandler.installMapper(TAG_DIALECTSPECIFIC, new Mapper<DialectParam>(DialectParam.class)
             {
                @Override
                public void mergeSubObj(DialectParam d, Object c)
                {
                   checkSubObjType(c, Parameter.class);
                   d.addParameter((Parameter) c);
                }
             });

          // throwaway class used just to map the <table> tag so that we can
          // add a table name to the Metadata object in the Mapper below
          final class Table
          {
             private String name = null;

             public String getName()
             {
                return name;
             }

             public void setName(String name)
             {
                this.name = name;
             }
          }

          saxHandler.installMapper(TAG_METADATA, new Mapper<Metadata>(Metadata.class)
             {
                @Override
                public void mergeSubObj(Metadata m, Object c)
                {
                   checkSubObjType(c, Table.class);
                   m.addTable(((Table) c).getName());
                }
             });

          saxHandler.installMapper(TAG_TABLE, new Mapper<>(Table.class));

          // tags that don't map to objects:
          saxHandler.setUnhandledTags(new String[] {TAG_CFG, TAG_GLOBAL});

          // constraints:
          saxHandler.setConstraints(TAG_PARAMETER,
                                    new String[]{"name", "value"},
                                    null,
                                    new String[]{},
                                    new String[]{});

          saxHandler.setConstraints(TAG_INCLUDE_MAPPINGS,
                                    null,
                                    null,
                                    new String[]{TAG_PARAMETER},
                                    null);

          saxHandler.setConstraints(TAG_NAMESPACE,
                                    new String[]{"name"},
                                    new String[]
                                    {
                                       "importFile",
                                       /*deprecated*/"xmlFile",
                                       "conversion",
                                       "mutable",
                                       "default",
                                       "primary"
                                    },
                                    null,
                                    new String[]{TAG_PARAMETER, TAG_DIALECTSPECIFIC});

          saxHandler.setConstraints(TAG_DIALECTSPECIFIC,
                                    new String[]{"name"},
                                    new String[]{},
                                    new String[]{TAG_PARAMETER},
                                    new String[]{});

          saxHandler.setConstraints(TAG_METADATA, new String[] { "name" }, null, null, new String[] { "table" });

          saxHandler.setConstraints(TAG_TABLE, new String[] { "name" }, null, null, null);
       }

       /**
        * Parses a given XML data stream using the SAX parser and handler that were
        * already prepared. The objects built in the process are held in the
        * handler and can be fetched by XML tag name with {@link
        * GenericSAXHandler#getGlobalObjects(String)}.
        *
        * @param  cfgStream
        *         Input stream with data to be parsed.
        *
        * @throws IOException
        *         If any IO errors occur in reading the file.
        * @throws SAXException
        *         If any SAX errors occur during processing.
        * @throws RuntimeException
        *         wrapping a <code>ConfigurationException</code> if any error
        *         occurs reading the XML data or creating the configuration
        *         object. This includes missing configuration file or file format
        *         constraint violations detected during parsing.
        *
        * @see    GenericSAXHandler#getGlobalObjects(String)
        */
       private void loadXml(InputStream cfgStream)
       throws SAXException,
              IOException,
              RuntimeException
       {
          try
          {
             this.saxParser.reset();
             this.saxHandler.resetState();
             this.saxParser.parse(new InputSource(cfgStream), this.saxHandler);
          }
          finally
          {
             cfgStream.close();
          }
       }

       /**
        * Simple schema loader test: the XML file passed as an argument is parsed
        * and the resulting <code>SchemaConfig</code> together with all global
        * parameters are printed to standard output. Variable substitution is not
        * performed on parameters, their values are shown as they were read from
        * XML.
        *
        * @param  args
        *         The first element is the absolute path of the XML file to parse.
        *
        * @throws IOException
        *         If IO errors occur.
        * @throws SAXException
        *         If the SAX parser encounters problems while processing the XML
        *         content.
        * @throws RuntimeException
        *         wrapping a <code>ConfigurationException</code> if any error
        *         occurs reading the XML data or creating the configuration
        *         objects. This includes missing configuration file or file format
        *         constraint violations detected during parsing.
        */
       public static void main(String[] args)
       throws SAXException,
              IOException,
              RuntimeException
       {
          Configuration config = getInstance();

          // show the loaded schema
          System.out.println(config.schemaConfig);

          if (args.length == 0)
          {
             return;
          }

          //parse the specified file
          config.loadXml(Files.newInputStream(Paths.get(args[0])));

          // print the constructed objects:

          SchemaConfig sc = (SchemaConfig) config.saxHandler.getGlobalObjects(TAG_SCHEMA).get(0);
          LOG.info("Global schema object:\n" + sc);

          LOG.info("Global parameters:");
          List<?> params = getInstance().saxHandler.getGlobalObjects(TAG_PARAMETER);
          for (Object o : params)
          {
             Parameter p = (Parameter)o;
             LOG.info("name = " + p.getName() + ", value = " + p.getValue());
          }

          System.out.println("\nGlobal profiles:\n");
          List<?> profiles = getInstance().saxHandler.getGlobalObjects(TAG_PROFILE);
          for (Object o : profiles)
          {
             ProfileConfig profile = (ProfileConfig) o;

             System.out.println("Profile configuration for: " + profile.getName());

             System.out.println("Schema object:\n\n" + profile.getSchema());

             System.out.println("\nParameters:\n");
             Map<String, String> profileParams = profile.getParameters();
             for (Map.Entry<String, String> p : profileParams.entrySet())
             {
                System.out.println("name = " + p.getKey() + ", value = " + p.getValue());
             }

             System.out.println("---------------------------------\n\n");
          }
       }

       /**
        * Mapping of include file path prefixes as found in source file include references, to new
        * prefixes, which will allow include files to be found in the conversion project directory
        * hierarchy. The replacement is commonly empty string, to make an absolute file reference
        * in the original code into a relative file reference. However, it could be something else.
        * This facility is not meant to replace the PROPATH, but rather to adjust for cases (e.g.,
        * absolute include file references) where the source code might use include file references
        * which are specific to a particular deployment which is not general purpose, and the PROPATH
        * alone is not enough.
        */
       public static class IncludeRemap
       {
          /** Mapping of old include file prefixes to new ones */
          private final Map<String, String> map = new LinkedHashMap<>();

          /**
           * Remap an old include file prefix to a new one.
           *
           * @param   prefix
           *          Old prefix as found in a source code include reference.
           * @param   replacement
           *          Prefix replacement. Set to empty string to remove a prefix entirely.
           */
          public void remap(String prefix, String replacement)
          {
             map.put(prefix, replacement);
          }

          /**
           * Get the mapping of old to new include file prefixes.
           *
           * @return  Include file prefix map.
           */
          Map<String, String> getMap()
          {
             return map;
          }
       }
    }