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;
      }
   }
}