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>
* <cfg>
* <global>
* <parameter name="user.home" value="/home/user" />
* <parameter name="subdir" value="${user.home}/mysubdir" />
* </global>
* ...
* </cfg>
* </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><root>.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;
}
}
}