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