SchemaConfig.java

/*
** Module   : SchemaConfig.java
** Abstract : Schema configuration information
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20041217   @19174 Created initial version.
** 002 ECF 20050207   @19639 Added support for schema metadata config information. Added two public methods:
**                           getMetadataFile and setMetadata.
** 003 ECF 20050411   @20707 Added import filename to NsConfig, along with getter and setter methods.
** 004 GES 20070402   @32704 Added a preferred flag to the namespace configuration. This can be used to
**                           resolve some ambiguous name conflicts.
** 005 CA  20101102          Inferred generics.Added support for new namespace parameters, under the namespace
**                           node: ddl-dialects parameter node and dialect-specific node.
** 006 LMR 20110106          Added toString() method to print the whole object graph.
** 007 ECF 20130125          Changed and expanded metadata configuration to reflect updated
**                           structure of p2j.cfg.xml. Added Metadata inner class, which reflects
**                           addition of metadata element in the XML.
** 008 ECF 20130804          Added standard set of metadata table names that are always needed at
**                           runtime, so that downstream schema processing will convert these.
**                           Trim and lowercase metadata table names for safety. Added method getSchemas.
** 009 SVL 20140320          Added getSchemaResource.
** 010 ECF 20141123          Partially backed out 008: removed standard set of metadata table names.
** 011 ECF 20150328          Fixed bug in getSchemas which did not properly omit metadata schema.
** 012 ECF 20150910          Added isMetadataActive.
** 013 EVL 20160225          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 014 IAS 20160509          Fixed metadata processing in case of more than one default namespace.
** 015 OM  20180118          Removed notion of primary database.
** 016 IAS 20200617          Added mandatory meta tables support.
** 017 ECF 20200312          Added configurable primary key name.
** 018 IAS 20200622          Re-worked metadata configuration in p2j.cfg.xml.
**     OM  20210902          Computed "importFile" and "xmlFile" attributes' implied values if not explicitly
**                           set in p2j.cfg.xml.
**     OM  20210908          [nsConfigs] uses normalized (lowercased) keys.
**     OM  20210920          Added support for grouping namespaces into profiles and select one from command
**                           line parameter.
**     ECF 20210927          Fixed typo.
**     OM  20211105          Dropped 'fwd.profile.default' check from profile selection algorithm.
**     OM  20220325          Added dataFolder for specifying the optional location of data to be imported.
**     OM  20220330          Moved database conversion artifacts to ${cvtpath} folder.
**     OM  20220412          Developer mode: allows to load resources from file system if jar is not built.
**     OM  20220422          Developer mode: decreased the amount of messages. The default value of [default]
**                           attribute for namespaces is [true]. The [standard] namespace is pre-defined.
**     OM  20220428          Invoked prepareStandardNamespace() for all [Profile] constructors.
**     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 20220515          Added SQL name conversion mode to NsConfig.
**     ECF 20230127          Added mutable schema configuration option to NsConfig.
** 019 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 020 CA  20230705          For the 'standard' namespace, the 'default database' value must be 'false'.
** 021 ICP 20240705          Fixed method of obtaining the class loader.
** 022 DDF 20250219          Replaced mandatoryTables() call with filterTables().
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.schema;

import java.io.*;
import java.util.*;
import java.util.logging.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.cfg.Parameter;
import com.goldencode.p2j.classloader.MultiClassLoader;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.persist.meta.MetadataManager.*;
import com.goldencode.p2j.util.*;

/**
 * Schema configuration information. This class is instantiated by the master {@link
 * com.goldencode.p2j.cfg.Configuration Configuration} reading input data from an XML
 * configuration file.  The syntax of that XML file is specified external to this class.  The
 * purpose of this class is to store and provide access to the configuration.  How that
 * configuration is read is not of concern here.
 * <p>
 * Each database used by an application is configured in an XML element in the configuration file.
 * That element contains a short name or handle for the database, which matches the logical name
 * of the database. It also contains a path to the DF file exported from the Progress data
 * dictionary (the import file from our perspective), as well as the XML file to which the
 * imported schema information will be written for use in conversion (the schema file). Finally, a
 * database can be a default database (always loaded into the schema dictionary when parsing
 * source code), or non-default (only loaded on demand by the Progress parser). All of this
 * information about a particular database is encoded in an instance of the <code>NsConfig</code>
 * inner class (which is <code>public</code> since the external configuration reading process must
 * create instances of it). Those <code>NsConfig</code> instances are then added using {@link
 * #addNamespace}.
 * <p>
 * One database is designated the "metadata" database. This is the database which contains
 * metadata about the schema structure used to define all databases in Progress (for a specific
 * version of Progress). The tables and fields of this database are referenced in source code as
 * if they were members of every other logical database. Thus, the schema information included in
 * the metadata database schema is merged with each logical database as it is loaded into the
 * schema dictionary. The metadata schema configuration is accessible via the {@link #getMetadata}
 * method.
 *
 * @see  SchemaLoader
 * @see  SchemaDictionary
 */
public final class SchemaConfig
{
   /** Logger for objects of this type. */
   private static final ConversionStatus LOG = ConversionStatus.get(SchemaConfig.class);
   
   /** Flag used for simplify the log by avoiding multiple messages for trying developing mode search. */
   private static boolean developerModeWarned = false;
   
   /** Name of metadata definition applied to every loaded schema */
   private Metadata metadata;
   
   /** Map of database names to namespace configuration data. The key in this map are case-insensitive. */
   private final Map<String, Profile> profiles = new LinkedHashMap<>();
   
   /**
    * This is the unnamed profile. It contains all namespaces located directly in the {@code schema} node.
    * This is the best configuration in a single profile configuration. Also, if there are some namespaces
    * defined here and no other profiles was marked as default otherwise, this will become the default
    * profile.
    */
   private final Profile unnamedProfile = new Profile(null);
   
   /** This is the default profile when the configuration is queried. */
   private Profile defaultProfile = null;
   
   /** Primary key name, hardcoded the same as {@link com.goldencode.p2j.persist.orm.Session#DEFAULT_PK} */
   private String primaryKeyName = "recid";
   
   /**
    * Default constructor.
    */
   public SchemaConfig()
   {
      this.metadata = new Metadata("standard", Collections.emptySet(), false);
   }
   
   /**
    * Determine whether metadata is active for the current application. Metadata is considered
    * active if at least one metadata table is configured.
    * 
    * @return  <code>true</code> if the application uses metadata, else <code>false</code>.
    */
   public boolean isMetadataActive()
   {
      return metadata != null && metadata.isActive(); 
   }
   
   /**
    * Get the metadata identifier name associated with all schemas.
    *
    * @return  Common schema metadata identifier.
    */
   public Metadata getMetadata()
   {
      return metadata;
   }
   
   /**
    * Name of metadata definition applied to every loaded schema.
    *
    * @param   metadata
    *          Metadata definition name.
    */
   public void setMetadata(Metadata metadata)
   {
      this.metadata = metadata;
   }
   
   /**
    * Get the name of the surrogate primary key column used by the current project.
    * 
    * @return  Surrogate primary key name.
    */
   public String getPrimaryKeyName()
   {
      return primaryKeyName;
   }
   
   /**
    * Set the name of the surrogate primary key name, overriding the default name of {@code recid}.
    * 
    * @param   primaryKeyName
    *          New surrogate primary key name.
    */
   public void setPrimaryKeyName(String primaryKeyName)
   {
      this.primaryKeyName = primaryKeyName;
   }
   
   /**
    * Get a list of schema names of the default profile, optionally omitting the name of the metadata schema.
    * 
    * @param   omitMeta
    *          {@code true} to omit the metadata schema, {@code false} to include it.
    */
   public List<String> getSchemas(boolean omitMeta)
   {
      return getSchemas(null, omitMeta);
   }
   
   /**
    * Get a list of schema names, optionally omitting the name of the metadata schema.
    * 
    * @param   profile
    *          The profile to be processed.
    * @param   omitMeta
    *          {@code true} to omit the metadata schema, {@code false} to include it.
    */
   public List<String> getSchemas(String profile, boolean omitMeta)
   {
      Set<String> keys = getProfile(profile, false).nsConfigs.keySet();
      ArrayList<String> list = new ArrayList<>(keys.size());
      
      if (omitMeta)
      {
         for (String next : keys)
         {
            if (metadata.getName().equals(next))
            {
               continue;
            }
            
            list.add(next);
         }
      }
      else
      {
         list.addAll(keys);
      }
      
      list.trimToSize();
      
      return list;
   }
   
   /**
    * Get an iterator for the configured database names of default profile.
    *
    * @return  Object which iterates over configured database names.
    */
   public Iterator<String> databases()
   {
      return databases(false);
   }
   
   /**
    * Get an iterator for the configured database names of all activated profiles.
    *
    * @return  Object which iterates over configured database names.
    */
   public Iterator<String> databases(boolean active)
   {
      if (active)
      {
         Set<String> dbs = new HashSet<>();
         for (Profile profile : profiles.values())
         {
            if (profile.active)
            {
               profile.nsConfigs.keySet().forEach((db) -> dbs.add(db));
            }
         }
         return dbs.iterator();
      }
      
      return databases(null);
   }
   
   /**
    * Get an iterator for the configured database names of the requested profile.
    *
    * @param   profile
    *          The profile to be processed. If {@code null} the default profile will be used.
    *
    * @return  Object which iterates over configured database names.
    */
   public Iterator<String> databases(String profile)
   {
      return getProfile(profile, false).nsConfigs.keySet().iterator();
   }
   
   /**
    * Get an iterator of names of those databases which have been configured as default schemas for namespace
    * lookup purposes. These are databases which are loaded automatically by a {@link SchemaDictionary} object
    * at construction.
    *
    * @return  Object which iterates over the names of default databases.
    */
   public Iterator<String> defaultDatabases()
   {
      return defaultDatabases(null);
   }
   
   /**
    * Get an iterator of names of those databases which have been configured as default schemas for namespace
    * lookup purposes. These are databases which are loaded automatically by a {@link SchemaDictionary} object
    * at construction.
    *
    * @param   profile
    *          The profile to be processed. If {@code null} the default profile will be used.
    *
    * @return  Object which iterates over the names of default databases.
    */
   public Iterator<String> defaultDatabases(String profile)
   {
      // Return instance of anonymous nested iterator.
      return new Iterator<String>()
      {
         /** Name of next default database to be returned by iterator */
         private String nextName = null;
         
         /** Iterator of all database names */
         private final Iterator<Map.Entry<String, NsConfig>> iter = 
               getProfile(profile, false).nsConfigs.entrySet().iterator();
         
         /**
          * Determine whether there is another default database over which
          * to iterate.
          *
          * @return  <code>true</code> if iteration can continue, else
          *          <code>false</code>.
          */
         public boolean hasNext()
         {
            while (iter.hasNext())
            {
               Map.Entry<String, NsConfig> entry = iter.next();
               NsConfig cfg = entry.getValue();
               if (cfg.isDefault())
               {
                  nextName = cfg.getName();
                  return true;
               }
            }
            
            nextName = null;
            return false;
         }
         
         /**
          * Return the next default database name in the iterator's sequence.
          *
          * @return  Next name.
          */
         public String next()
         {
            return nextName;
         }
         
         /**
          * Unsupported remove operation.
          *
          * @throws  UnsupportedOperationException
          *          This {@code Exception} is always thrown when the method is invoked.
          */
         public void remove()
         {
            throw new UnsupportedOperationException();
         }
      };
   }
   
   /**
    * Sets the default configuration profile to be used during this process. This profile is used when the
    * methods of {@code SchemaConfig} which do not have the profile as parameter as the default referrer.
    * <p>
    * It is illegal to call this method twice during a process runtime. It is illegal to call this method
    * after the object has already accessed with default getters. If this method was not called, when a
    * default getter is invoked, a {@code defaultProfile} is selected automatically using the algorithm
    * described in {@link #getDefaultProfile()}.
    * 
    * @param   profileName
    *          The name of the profile to be set as default. 
    */
   public void setDefaultProfile(String profileName)
   {
      defaultProfile = profileName == null ? null : profiles.get(profileName.toLowerCase());
      if (defaultProfile != null)
      {
         LOG.log(Level.INFO, "Configuration profile set to '" + defaultProfile.name + "'.");
      }
      else
      {
         LOG.log(Level.WARNING,
                 "Failed to set configuration profile (" + profileName + "). Will use auto-detection.");
      }
   }
   
   /**
    * Add a profile object to this configuration. Called by the configuration processor when reading the XML
    * schema configuration.
    *
    * @param   config
    *          The profile configuration.
    */
   public void addProfile(ProfileConfig config)
   {
      Profile profile = new Profile();
      profile.setName(config.getName());
      profile.setDefault(config.isDefault());
      
      SchemaConfig profileSchema = config.getSchema();
      if (profileSchema != null)
      {
         for (NsConfig cfg : profileSchema.unnamedProfile.nsConfigs.values())
         {
            profile.addNamespace(cfg);
         }
      }
      
      profiles.put(config.getName(), profile);
   }

   /**
    * Mark the specified profile as active and return all its configured namespaces.
    * 
    * @param    profile
    *           The profile name.
    *           
    * @return   The set of namespaces.
    */
   public Set<String> setActive(String profile)
   {
      Set<String> res = new HashSet<>();
      
      Profile p = profiles.get(profile.toLowerCase());
      p.active = true;
      p.nsConfigs.values().forEach(cfg -> res.add(cfg.name.toLowerCase()));
      
      return res;
   }
   
   /**
    * Deactivate all default profiles, as we are running in multi-profile mode.  In this mode, each profile
    * can be set as default, when a resource belonging to it gets processed (like a schema or a file).
    */
   public void deactiveDefaults()
   {
      profiles.values().forEach(p -> p.setDefault(false));
   }
   
   /**
    * Add a namespace configuration object to the default profile configuration. Called by the configuration
    * processor when reading the XML schema configuration.
    *
    * @param   cfg
    *          Namespace configuration object.
    */
   public void addNamespace(NsConfig cfg)
   {
      unnamedProfile.nsConfigs.put(cfg.getName().toLowerCase(), cfg);
      profiles.put(null, unnamedProfile);
   }
   
   /**
    * Returns a file object representing the import (DF) file associated with the specified database schema
    * name of the default profile. The file's path is fully qualified.
    *
    * @param   nsName
    *          Name of desired namespace configuration
    *
    * @return  Import file.
    *
    * @throws  ConfigurationException
    *          if environment has not been configured correctly.
    */
   public File getImportFile(String nsName)
   throws ConfigurationException
   {
      return getImportFile(null, nsName);
   }
   
   /**
    * Returns a file object representing the import (DF) file associated with the specified database schema
    * name in a specific profile. The file's path is fully qualified.
    *
    * @param   profile
    *          The profile to be processed. If {@code null} the default profile will be used.
    * @param   nsName
    *          Name of desired namespace configuration
    *
    * @return  Import file.
    *
    * @throws  ConfigurationException
    *          if environment has not been configured correctly.
    */
   public File getImportFile(String profile, String nsName)
   throws ConfigurationException
   {
      NsConfig cfg = getProfile(profile, false).nsConfigs.get(nsName == null ? null : nsName.toLowerCase());
      return (cfg != null) ? Configuration.toFile(cfg.getImportFile()) : null;
   }
   
   /**
    * Returns the value of the {@code mutable} flag associated with the specified database schema name in a
    * specific profile.
    *
    * @param   nsName
    *          Name of desired namespace configuration.
    *
    * @return  {@code true} if the schema for the specified namespace configuration is mutable; {@code false}
    *          if it is not mutable, or if the specified namespace does not exist.
    *
    * @throws  ConfigurationException
    *          if environment has not been configured correctly.
    */
   public boolean isMutable(String nsName)
   throws ConfigurationException
   {
      NsConfig cfg = getProfile(null, false).nsConfigs.get(nsName == null ? null : nsName.toLowerCase());
      return (cfg != null) ? cfg.isMutable() : false;
   }
   
   /**
    * Get the SQL name conversion override constant defined for a given schema namespace, if any.
    * 
    * @param   nsName
    *          Namespace name.
    * 
    * @return  SQL name conversion override mode.
    * 
    * @see     com.goldencode.p2j.convert.MatchPhraseConstants
    */
   public int getSqlConversion(String nsName)
   {
      NsConfig nscfg = getProfile(null, false).nsConfigs.get(nsName);
      
      return nscfg != null ? nscfg.conversion : MatchPhraseConstants.SQL_MODE_DEFAULT;
   }
   
   /**
    * Returns a file object representing the XML document associated with the specified database schema name
    * of the default profile. The file's path is fully qualified.
    *
    * @param   nsName
    *          Name of desired namespace configuration
    * @param   type
    *          the type of resource (the file extension, including the dot).
    *
    * @return  File containing namespace schema XML document.
    *
    * @throws  ConfigurationException
    *          if environment has not been configured correctly.
    */
   public File getSchemaFile(String nsName, String type)
   throws ConfigurationException
   {
      return getSchemaFile(null, nsName, type);
   }
   
   /**
    * Returns a file object representing the XML document associated with the specified database schema name
    * of the specific profile. The file's path is fully qualified.
    *
    * @param   profile
    *          The profile to be processed. If {@code null} the default profile will be used.
    * @param   nsName
    *          Name of desired namespace configuration
    * @param   type
    *          the type of resource (the file extension, including the dot).
    *
    * @return  File containing namespace schema XML document.
    *
    * @throws  ConfigurationException
    *          if environment has not been configured correctly.
    */
   public File getSchemaFile(String profile, String nsName, String type)
   throws ConfigurationException
   {
      NsConfig cfg = getProfile(profile, false).nsConfigs.get(nsName == null ? null : nsName.toLowerCase());
      if (cfg == null)
      {
         return null;
      }
      
      return Configuration.toFile(getSchemaFileName(profile, nsName, type));
   }
   
   /**
    * Returns path to the resource representing the XML document associated with the specified
    * database schema name in the default profile.
    *
    * @param   nsName
    *          Name of desired namespace configuration
    * @param   type
    *          the type of resource (the file extension, including the dot).
    *
    * @return  path to the resource containing namespace schema XML document or {@code null} if the resource
    *          doesn't exist.
    */
   public String getSchemaFileName(String nsName, String type)
   {
      return getSchemaFileName(null, nsName, type);
   }
   
   /**
    * Returns path to the resource representing the XML document associated with the specified database schema
    * name in the requested profile.
    *
    * @param   profile
    *          The profile to be processed. If {@code null} the default profile will be used.
    * @param   nsName
    *          Name of desired namespace configuration
    * @param   type
    *          the type of resource (the file extension, including the dot).
    *
    * @return  path to the resource containing namespace schema XML document or {@code null} if the resource
    *          doesn't exist.
    */
   public String getSchemaFileName(String profile, String nsName, String type)
   {
      NsConfig cfg = getProfile(profile, false).nsConfigs.get(nsName == null ? null : nsName.toLowerCase());
      if (cfg != null)
      {
         return cfg.getDataArtifactsLocation() + "/" + nsName + type;
      }
      
      return null;
   }
   
   /**
    * Returns path to the resource representing the XML document associated with the specified
    * database schema name in the default profile.
    *
    * @param   nsName
    *          Name of desired namespace configuration
    * @param   type
    *          the type of resource (the file extension, including the dot).
    *
    * @return  path to the resource containing namespace schema XML document or {@code null} if the resource
    *          doesn't exist.
    */
   public String getSchemaResource(String nsName, String type)
   {
      return getSchemaResource(null, nsName, type);
   }
   
   /**
    * Returns path to the resource representing the XML document associated with the specified database schema
    * name in the requested profile.
    *
    * @param   profile
    *          The profile to be processed. If {@code null} the default profile will be used.
    * @param   nsName
    *          Name of desired namespace configuration
    * @param   type
    *          the type of resource (the file extension, including the dot).
    *
    * @return  path to the resource containing namespace schema XML document or {@code null} if the resource
    *          doesn't exist.
    */
   public String getSchemaResource(String profile, String nsName, String type)
   {
      String resourceName = getSchemaFileName(profile, nsName, type);
      
      if (resourceName != null)
      {
         if (MultiClassLoader.getClassLoader().getResource(resourceName) != null)
         {
            return resourceName;
         }
         
         // making the best effort of loading the resource from file system to allow developers to get it
         // while working on IDE, without the need to have a full application jar built
         if (!developerModeWarned)
         {
            LOG.log(Level.SEVERE,  "Failed to locate '" + resourceName + "' as a resource. Using developer " +
               "mode: attempting to locate it as local file in ${cvtpath} location.");
            developerModeWarned = true;
         }
         
         try
         {
            File toFile = Configuration.toFile(Configuration.getParameter("cvtpath") + "/" + resourceName);
            if (toFile.exists())
            {
               return toFile.getCanonicalPath();
            }
         }
         catch (ConfigurationException | IOException e)
         {
            LOG.log(Level.SEVERE, "Failed to locate '" + resourceName + "' resource as a local file.");
            return null;
         }
      }
      
      return null;
   }
   
   /**
    * Reports whether the database should be preferred in cases where its entities are involved in
    * ambiguous name conflicts.
    *
    * @param   nsName
    *          Name of desired namespace configuration
    *
    * @return  {@code true} if the database is preferred.
    */
   public boolean isPreferred(String nsName)
   {
      return isPreferred(null, nsName);
   }
   
   /**
    * Reports whether the database should be preferred in cases where its entities are involved in
    * ambiguous name conflicts.
    *
    * @param   profile
    *          The profile to be processed. If {@code null} the default profile will be used.
    * @param   nsName
    *          Name of desired namespace configuration
    *
    * @return  {@code true} if the database is preferred.
    */
   public boolean isPreferred(String profile, String nsName)
   {
      NsConfig cfg = getProfile(profile, false).nsConfigs.get(nsName == null ? null : nsName.toLowerCase());
      return cfg != null && cfg.isPreferred();
   }
   
   /**
    * Get the value for the given namespace parameter in the default profile.
    * 
    * @param   name
    *          The namespace name.
    * @param   param
    *          The parameter name or path.
    *
    * @return  The parameter's value or {@code null} if {@code name} or {@code param} are invalid. 
    */
   public String getNamespaceParameter(String name, String param)
   {
      return getNamespaceParameter(null, name, param);
   }
   
   /**
    * Get the value for the given namespace parameter in the specific profile.
    *
    * @param   profile
    *          The profile to be processed. If {@code null} the default profile will be used.
    * @param   name
    *          The namespace name.
    * @param   param
    *          The parameter name or path.
    *
    * @return  The parameter's value or {@code null} if {@code name} or {@code param} are invalid. 
    */
   public String getNamespaceParameter(String profile, String name, String param)
   {
      NsConfig cfg = getProfile(profile, false).nsConfigs.get(name == null ? null : name.toLowerCase());
      return cfg == null ? null : cfg.getParameter(param);
   }
   
   /**
    * Returns a string representation of this {@code SchemaConfig}'s object graph.
    * 
    * @return  A string representation of this object.
    * 
    * @see Object#toString()
    */
   @Override
   public String toString()
   {
      StringBuilder sb = new StringBuilder("schema:");
      sb.append("\n  primary key name = ").append(primaryKeyName);
      sb.append("\n  metadata = ").append(getMetadata());
      sb.append("\n  profiles: ");
      
      for (Profile p : profiles.values())
      {
         sb.append("\n").append(p.name == null ? "<unnamed>" : p.name);
         if (p.isDefault)
         {
            sb.append("*");
         }
         sb.append("\n    namespaces: ");
         for (NsConfig ns : p.nsConfigs.values())
         {
            sb.append("\n      ");
            sb.append(ns.getName());
            sb.append(":");
            sb.append("\n        importFile = ").append(ns.getImportFile());
            sb.append("\n        *dataArtifacts = ").append(ns.getDataArtifactsLocation());
            sb.append("\n        mutable = ").append(ns.isMutable());
            sb.append("\n        default = ").append(ns.isDefault());
            sb.append("\n        conversion = ").append(ns.getConversion());
            sb.append("\n        preferred = ").append(ns.isPreferred());
            sb.append("\n        parameters:");
            for (Map.Entry<String, String> e : ns.parameters.entrySet())
            {
               sb.append("\n          [").append(e.getKey()).append("] = ").append(e.getValue());
            }
         }
      }
      
      return sb.toString();
   }
   
   /**
    * Returns the default profile. If no profile was programmatically configured as default, the following
    * algorithm will be used to select one:
    * <ol>
    *    <li>scan for <strong>first</strong> profile having the {@code default} attribute set;</li>
    *    <li>use the {@code unnamed} profile <strong>if not empty</strong>. The unnamed profile is the profile
    *        composed from the namespaces located directly in {@code schema} tag, at the same level with the
    *        other named {@code profile}-s. In case there are no named profiles, this cause the unnamed
    *        profile to be the default, naturally matching the old configuration files where profiles were not
    *        supported (backward compatibility); </li>
    *    <li>scan for <strong>first</strong> available profile if any named profile is defined;</li>
    *    <li>otherwise, use the {@code unnamed} profile</li>
    * </ol>
    * 
    * @return the default profile. Never {@code null}.
    */
   private Profile getDefaultProfile()
   {
      if (defaultProfile == null)
      {
         for (Profile p : profiles.values())
         {
            if (p.isDefault)
            {
               defaultProfile = p;
               break;
            }
         }
         
         // node declared as default?
         if (defaultProfile == null)
         {
            if (!unnamedProfile.nsConfigs.isEmpty() || profiles.isEmpty())
            {
               defaultProfile = unnamedProfile;
            }
            else
            {
               // tough, pick one at random, first?
               for (Profile p : profiles.values())
               {
                  defaultProfile = p;
                  break;
               }
            }
         }

         LOG.log(Level.INFO,
                 "Using " +
                    (defaultProfile.name == null ? "unnamed" : defaultProfile.name) +
                    " schema profile.");
      }
      
      return defaultProfile;
   }
   
   /**
    * Obtain a profile based on its name.
    * 
    * @param   profile
    *          The name of the profile to be returned. If {@code null}, the unnamed profile is returned.
    * @param   read
    *          Use {@code true} is the configuration is read from file, during the {@code SchemaConfig}
    *          construction. Use {@code false} when accessing from a getter.
    *
    * @return  The requested profile, as described above.
    */
   private Profile getProfile(String profile, boolean read)
   {
      Profile profileObj;
      if (profile == null)
      {
         profileObj = read ? unnamedProfile : getDefaultProfile();
      }
      else
      {
         profileObj = profiles.get(profile.toLowerCase());
         if (profileObj == null)
         {
            profileObj = read ? unnamedProfile : getDefaultProfile();
         }
      }
      return profileObj;
   }
   
   /**
    * Encapsulates information about a namespace configuration, including name, XML file, and whether
    * namespace should be loaded into the schema dictionary by default.
    * <p>
    * This class is public since it must be accessed as part of the public API for the containing class.
    */
   public static class NsConfig
   {
      /** Identifying name for namespace. */
      private String name;
      
      /** Whether the schema is mutable (it can be modified externally and is scanned at runtime) */
      private boolean mutable = false;
      
      /** Name of {@code .df} import file. */
      private String importFile;
      
      /** SQL name conversion override for SQL table and column names. */
      private int conversion = MatchPhraseConstants.SQL_MODE_DEFAULT;
      
      /** Flag indicating if namespace is loaded by default. */
      private boolean defDb = true;
      
      /** Namespace is preferred in ambiguous name conflicts. */
      private boolean prefer = false;
      
      /** Namespace parameters. */
      private final Map<String, String> parameters = new HashMap<>();
      
      /**
       * Default constructor.
       */
      public NsConfig()
      {
      }
      
      /**
       * Get identifying namespace name.
       *
       * @return  Namespace name.
       */
      public String getName()
      {
         return name;
      }
      
      /**
       * Set name.
       *
       * @param   name
       *          Namespace name.
       */
      public void setName(String name)
      {
         this.name = name;
         
         if ("standard".equals(name))
         {
            this.defDb = false;
         }
      }
      
      /**
       * Get the mutable flag.
       * 
       * @return  {@code true} if the database schema is mutable, else {@code false}.
       */
      public boolean isMutable()
      {
         return mutable;
      }
      
      /**
       * Set the mutable flag.
       * 
       * @param   mutable
       *          {@code true} if the database schema is mutable, else {@code false}.
       */
      public void setMutable(boolean mutable)
      {
         this.mutable = mutable;
      }
      
      /**
       * Get DF import filename, defaulting to standard location for {@code .df} files if not explicitly
       * configured in {@code p2j.cfg.xml}.
       *
       * @return  Import filename.
       */
      public String getImportFile()
      {
         if (importFile != null)
         {
            return Configuration.getInstance().substituteParms(importFile);
         }
         return "data/" + name + ".df";
      }
      
      /**
       * Set DF import filename.
       *
       * @param   importFile
       *          Import filename.
       */
      public void setImportFile(String importFile)
      {
         this.importFile = importFile;
      }
      
      /**
       * Get the type of conversion performed on SQL names.
       * 
       * @return  Type of conversion performed on SQL names.
       */
      public String getConversion()
      {
         switch (conversion)
         {
            case MatchPhraseConstants.SQL_MODE_MINIMAL:
               return "minimal";
            case MatchPhraseConstants.SQL_MODE_VERBATIM:
               return "verbatim";
            default:
               return null;
         }
      }
      
      /**
       * Set the type of conversion performed on SQL names.
       * 
       * @param   conversion
       *          Type of conversion performed on SQL names.
       */
      public void setConversion(String conversion)
      {
         switch (conversion)
         {
            case "minimal":
               this.conversion = MatchPhraseConstants.SQL_MODE_MINIMAL;
               break;
            case "verbatim":
               this.conversion = MatchPhraseConstants.SQL_MODE_VERBATIM;
               break;
            default:
               throw new IllegalArgumentException("Unknown SQL conversion override attribute: " +
                                                  conversion +
                                                  "'");
         }
      }
      
      /**
       * Get the location for {@code .dict}, {@code .schema}, and {@code .p2o} files of this namespace.
       * This is the same as the directory of the import file, relative to application's jar root at runtime
       * and relative to {@code $cvtpath} at conversion time.
       *
       * @return  as described above.
       */
      public String getDataArtifactsLocation()
      {
         // use the import file (.df) to compute the path relative to project's root
         String importFile = getImportFile().replace('\\', '/');
         int i = importFile.lastIndexOf("/");
         
         String path = ""; 
         if (i >= 0)
         {
            path = importFile.substring(0, i); // drop the file name
         }
         // else there is no path, just the filename so the target is the root
         
         if (Configuration.isRuntimeConfig() || Configuration.isImportMode())
         {
            return path; // this is relative to jar's root
         }
         
         // at conversion time take into account the full absolute path configured in [cvtpath]
         try
         {
            return Configuration.getConversionFolder() + "/" + path;
         }
         catch (ConfigurationException e)
         {
            LOG.log(Level.SEVERE,
                    "Failed to locate the \"cvtpath\" parameter in " + Configuration.DEF_CFG_FILE,
                    e);
            throw new RetryUnwindException(e);
         }
      }
      
      /**
       * Is this namespace loaded in the schema dictionary by default?
       *
       * @return  <code>true</code> if namespace is loaded by default, else
       *          <code>false</code>.
       */
      public boolean isDefault()
      {
         return defDb;
      }
      
      /**
       * Set default flag for this namespace configuration.
       *
       * @param   defDb
       *          <code>true</code> if namespace is loaded by default, else
       *          <code>false</code>.
       */
      public void setDefault(boolean defDb)
      {
         this.defDb = defDb;
      }
      
      /**
       * Is this namespace preferred when its components are involved in
       * ambiguous name conflicts?
       *
       * @return  <code>true</code> if namespace is preferred, else
       *          <code>false</code>.
       */
      public boolean isPreferred()
      {
         return prefer;
      }
      
      /**
       * Set preferred flag for this namespace configuration.  This can 
       * resolve ambiguous name conflicts if this namespace is the only one
       * that is preferred out of all the conflicting names.
       *
       * @param   prefer
       *          <code>true</code> if namespace is preferred, else
       *          <code>false</code>.
       */
      public void setPreferred(boolean prefer)
      {
         this.prefer = prefer;
      }
      
      /**
       * Notify of the extra namespace parameter instead of abending the application. This method (and the
       * {@code "xmlFile"} from the namespace options list (see its XML setup in
       * {@code Configuration.setupHandler}) will be removed in a future revision.
       *
       * @param   xmlFile
       *          ignored
       */
      public void setXmlFile(String xmlFile)
      {
         LOG.log(Level.SEVERE,
                 "Deprecated namespace parameter 'xmlFile'. Please remove it from " + Configuration.DEF_CFG_FILE);
      }
      
      /**
       * Add the specified parameter to the namespace.
       * 
       * @param   param
       *          The parameter.
       */
      public void addParameter(Parameter param)
      {
         parameters.put(param.getName(), param.getValue());
      }
      
      /**
       * Add the specified dialect parameters to the namespace.
       * <p>
       * All the parameter's names will follow the <code>[dialect]/[parameter]</code> syntax.
       * 
       * @param   dialectParam
       *          The dialect parameter.
       */
      public void addDialectParameter(DialectParam dialectParam)
      {
         // merge the params to the main map
         parameters.putAll(dialectParam.parameters);
      }
      
      /**
       * Get the value for the specified namespace parameter.
       * 
       * @param    param
       *           The parameter name.
       *           
       * @return   The parameter's value.
       */
      public String getParameter(String param)
      {
         return parameters.get(param);
      }
      
      /**
       * Helper class to read and set the dialect parameter. 
       */
      public static class DialectParam
      {
         /** The dialect alias. */
         private String name;
         
         /** The dialect's parameters. */
         private final Map<String, String> parameters = new HashMap<>();
         
         /**
          * Get the dialect alias.
          * 
          * @return   the dialect alias.
          */
         public String getName()
         {
            return name;
         }
         
         /**
          * Set the dialect alias.
          * 
          * @param   name
          *          the dialect alias.
          */
         public void setName(String name)
         {
            this.name = name;
         }
         
         /**
          * Add a parameter for this dialect alias. The parameter name will be
          * added the <code>[dialect]/</code> prefix.
          * 
          * @param   param
          *          The parameter.
          */
         public void addParameter(Parameter param)
         {
            parameters.put(name + "/" + param.getName(), param.getValue());
         }
      }
   }
   
   /**
    * Encapsulates information about the project's use of database metadata, including the name of
    * the metadata schema and the metadata tables used by the application, if any.
    * <p>
    * This class is public since it must be accessed as part of the public API for the containing
    * class.
    */
   public static class Metadata
   {
      /** Root name of the metadata schema (without any file extension) */
      private String name;
      
      /** Names of the tables in use by the application */
      private final Set<String> tables;
      
      /** Flag indicating that metadata is active */
      private final boolean active;
      
      /**
       * Default constructor.
       */
      public Metadata()
      {
         this(null, new HashSet<>(SystemTable.filterTables(SystemTable.MANDATORY)), true);
      }
      
      /**
       * Constructor.
       * 
       * @param   name
       *          The root name of the metadata schema.
       * @param   tables
       *          The names of the tables in use by the application.
       * @param   active
       *          A flag indicating that metadata is active.
       */
      public Metadata(String name, Set<String> tables, boolean active)
      {
         this.name = name;
         this.tables = tables;
         this.active = active;
      }
      
      /**
       * Get the root name of the metadata schema.
       * 
       * @return  Root name of the metadata schema.
       */
      public String getName()
      {
         return name;
      }
      
      /**
       * Set the root name of the metadata schema.
       * 
       * @param   name
       *          Root name of the metadata schema.
       */
      public void setName(String name)
      {
         this.name = name;
      }
      
      /**
       * Get an unmodifiable set of the names of the metadata tables used by the application.
       * 
       * @return  Metadata table names.
       */
      public Set<String> getTables()
      {
         return Collections.unmodifiableSet(tables);
      }
      
      /**
       * Add a table name to the set of metadata tables used by the application.
       * 
       * @param   table
       *          Metadata table name.
       */
      public void addTable(String table)
      {
         tables.add(table.trim().toLowerCase());
      }
      
      /**
       * Check if metadata is active.
       *
       * @return  {@code true} if metadata is active
       */
      public boolean isActive()
      {
         return active;
      }
      
      @Override
      public String toString()
      {
         return "Metadata(" + name + "): " + tables;
      }
   }
   
   /**
    * An immutable structure which holds information about a single configuration profile. There is always at
    * least one profile. Even if no {@code <profile>} tag is defined inside {@code <schema>} tag, all
    * {@code <namespace>}-s at that level form a single unnamed profile.
    */
   private static class Profile
   {
      /** Flag indicating if this profile is active in a multi-profile configuration. */
      private boolean active = false;;

      /** The name of the profile. */
      private String name = null;
      
      /** Flags this profile to be used by default. */
      private boolean isDefault = false;
      
      /** The map of namespaces in this profile .*/
      private final Map<String, NsConfig> nsConfigs = new LinkedHashMap<>();
      
      /**
       * Default constructor.
       */
      public Profile()
      {
         prepareStandardNamespace();
      }
      
      /**
       * Constructor
       *
       * @param   name
       *          The root name of the profile.
       */
      public Profile(String name)
      {
         this.name = name;
         prepareStandardNamespace();
      }
      
      /**
       * Sets the name of this profile. Only used by xml deserialization.
       * 
       * @param   name
       *          The name of the profile.
       */
      public void setName(String name)
      {
         this.name = name;
      }
      
      /**
       * Flags the profile as being the default.
       * 
       * @param   isDefault
       *          The new value for {@code default} flag of this profile.
       */
      public void setDefault(boolean isDefault)
      {
         this.isDefault = isDefault;
      }
      
      /**
       * Obtain the name of the profile.
       * 
       * @return  The name of the profile.
       */
      public String getName()
      {
         return name;
      }
      
      /**
       * Add a namespace configuration object to this profile. Called by the configuration processor when
       * reading the XML schema configuration.
       *
       * @param   cfg
       *          Namespace configuration object.
       */
      public void addNamespace(NsConfig cfg)
      {
         nsConfigs.put(cfg.getName().toLowerCase(), cfg);
      }
      
      /**
       * Prepare the default "standard" namespace with default values. Will be overwritten if the
       * configuration file contains an explicit definition for it.
       */
      private void prepareStandardNamespace()
      {
         // the [standard] namespace is optional. Add it now, temporarily with default values. If found later
         // while parsing the [p2j.cfg.xml], the space will be overwritten.
         NsConfig standard = new NsConfig();
         standard.setName("standard");
         standard.setDefault(false);
         addNamespace(standard);
      }
   }
}