NameConverterWorker.java
/*
** Module : NameConverterWorker.java
** Abstract : Exports name conversion methods to the pattern engine
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20050418 @20755 Created initial version. Exposes name
** conversion constants and methods for use
** within the pattern engine.
** 002 ECF 20050427 @20901 Enhancements. Shortened and lowercased
** constant names; namespace feature makes the
** shorter names adequately unique. Added an
** automatic report generation feature. Modified
** user function signatures to match constructor
** changes in NameConverter.
** 003 ECF 20050430 @20970 Shortened user function names to take
** advantage of namespace support. Also minor
** improvements to report generation.
** 004 ECF 20050526 @21290 Changes to support new expression engine
** implementation. Changed library registration
** mechanism based on superclass' modifications
** to support single library per pattern worker
** limit.
** 005 ECF 20050630 @21596 Refactored conversion API. Eliminated the
** multi-step conversion idiom in favor of a
** one-step mechanism in which custom
** replacements can be provided using varargs.
** 006 ECF 20050714 @21694 Added variant of convert user function. New
** version accepts a map of replacements which
** are loaded in addition to the defaults before
** conversion is performed.
** 007 GES 20051028 @23142 Added generateBeanName().
** 008 GES 20060311 @24976 Added load/reload of named match lists.
** 009 ECF 20060326 @25239 Fixed NPE in recordHistory() method.
** 010 GES 20090422 @41896 Converted to standard string formatting.
** 011 GES 20090424 @41931 Import change.
** 012 GES 20090518 @42381 Import change.
** 013 GES 20091117 @44420 Removed unused code.
** 014 GES 20140118 Added convertPackage() static method.
** 015 OM 20150525 Added resetDialects(String) to library exposed methods.
** Upgraded code to Java7 (generics, foreach, unboxing).
** 016 OM 20170207 Added TYPE_PROPERTY for special case of DMO property names.
** 017 OM 20171108 Avoided name collision between tables and sequences.
** 018 OM 20190829 Custom handling for hidden BEFORE-BUFFER fields.
** 019 ECF 20200906 New ORM implementation.
** OM 20210129 Avoided NPE for incorrectly initialized tables.
** IAS 20210219 Generic argument added.
** OM 20210908 When converting _temp or _meta database name, the name is kept.
** OM 20211020 Added _DATASOURCE_ROWID property.
** CA 20211214 The AST manager plugin must be context-local, for runtime conversion, as the
** InMemoryRegistryPlugin is not thread-safe.
** All worker's state must be context-local, for runtime conversion to work in
** multi-context mode, as the pattern workers are singletons.
** CA 20220202 Fixed a regression in CA/20211214, the defaultConverter must be re-initialized on
** each instance of this worker, as the defaults may have changed.
** OM 20220212 Use ReservedProperty static method for testing reserved properties names.
** OM 20220721 In case of TYPE_DENORMALIZED_PROPERTY the last _ (index separator) is preserved.
** ECF 20220514 Reworked to produce separate schema name reports for multiple schema inputs, and
** to release memory after each AST is processed.
** ECF 20220516 Added support for minimal change and verbatim SQL name conversion modes.
** OM 20230125 Avoid collisions of index names with SQL reserved keywords.
** 020 CA 20241119 A DMO alias must not emit as a FQL reserved keyword.
** 021 CA 20250401 Any possible Java import from core FWD packages can result in a 'schemaconflict',
** so the DMO name will be qualified if they match a simple core class name.
** 022 CA 20250515 Use legacy names for the proxies only if they can be used as-is as Java
** identifiers.
*/
/*
** 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.convert;
import java.io.*;
import java.util.*;
import com.goldencode.ast.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.security.*;
import com.goldencode.util.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.pattern.*;
/**
* Exports symbol name conversion functionality to the pattern matching
* engine for use within conversion rulesets. All symbol name conversion
* work is delegated to the {@link NameConverter} class.
* <p>
* An automatic name conversion reporting facility can be triggered from a
* ruleset. This mode is disabled by default. It is enabled with the {@link
* NameConverterWorker.Library#setReportMode} user function. As this worker
* converts names, a formatted history entry for each conversion is recorded.
* At the end of processing (i.e., in a post-rule ruleset), a text report is
* generated with {@link NameConverterWorker.Library#generateReport}. The
* report is written to a file which the caller specifies.
*/
public class NameConverterWorker
extends AbstractPatternWorker
implements MatchPhraseConstants
{
/** Format specifier for the column report output. */
private static final String RPT_FMT = "%-17s %-16s %-28s %-16s %s";
/** Map of conversion type literals, exposed by their constant names */
private static final Map<String, Long> TYPE_MAP = new HashMap<>();
/** Context local work area. */
private static final ContextLocal<WorkArea> context = new ContextLocal<WorkArea>()
{
protected WorkArea initialValue()
{
return new WorkArea();
}
};
// Populate the conversion type literal map.
static
{
TYPE_MAP.put("variable", (long) TYPE_VARIABLE);
TYPE_MAP.put("method", (long) TYPE_METHOD);
TYPE_MAP.put("constant", (long) TYPE_CONSTANT);
TYPE_MAP.put("class", (long) TYPE_CLASS);
TYPE_MAP.put("database", (long) TYPE_DATABASE);
TYPE_MAP.put("table", (long) TYPE_TABLE);
TYPE_MAP.put("column", (long) TYPE_COLUMN);
TYPE_MAP.put("index", (long) TYPE_INDEX);
TYPE_MAP.put("property", (long) TYPE_PROPERTY);
TYPE_MAP.put("dmo_alias", (long) TYPE_DMO_ALIAS);
TYPE_MAP.put("denormalized_property", (long) TYPE_DENORMALIZED_PROPERTY);
TYPE_MAP.put("standard", (long) STANDARD);
TYPE_MAP.put("custom", (long) CUSTOM);
TYPE_MAP.put("preserve", (long) PRESERVE);
TYPE_MAP.put("all", (long) ALL);
}
/**
* Default constructor which defines the symbol libraries to be registered.
*/
public NameConverterWorker()
{
super();
setLibrary(new Library());
// re-initialize the default context-local NameConverter, as the defaults may have changed. this is
// required for static conversion, as the datanames.xml is loaded when processing schema DMOs. for
// dynamic conversion, datanames.xml has no benefit, and is OK to not be used.
context.get().defaultConverter = new NameConverter();
}
/**
* This method is called each time the pattern engine needs to resolve
* a string constant into a numeric, boolean, or string literal value.
* In this case, it is used to resolve the conversion type literals to
* <code>Long</code> values, so that the conversion types can be
* referenced by their {@link NameConverter} constant names from within
* within user expressions.
* @param constant
* A string representing a conversion type constant to be
* resolved.
*
* @return A <code>Long</code> initialized to the corresponding constant
* value (values in <code>NameConverter</code> class), or
* <code>null</code> if <code>constant</code> was not recognized
* as a placeholder for such a literal.
*/
public Object resolveConstant(String constant)
{
return TYPE_MAP.get(constant.toLowerCase());
}
/**
* Called whenever the pattern engine visits a new AST, this method
* prepares the history report facility to receive entries for names
* converted while processing the AST. The history list is indexed in
* a map by the filename of the AST being visited.
*
* @param ast
* AST about to be processed by the pattern engine.
*/
@Override
public void visitAst(Aast ast)
{
String filename = ast.getFilename();
if (filename == null)
{
// this is the dynamic conversion case. The database/source is not available.
filename = "";
}
else
{
filename = filename.toLowerCase();
}
WorkArea wa = context.get();
wa.tables.computeIfAbsent(filename, k -> new HashSet<>());
if (wa.history != null)
{
wa.history.computeIfAbsent(filename, k -> new ArrayList<>());
}
}
/**
* Upon leaving an AST, if we are in report mode, reset name conversion history to allow data collection
* for the next (if any) AST to be processed.
*
* @param ast
* Copy AST just processed.
*/
@Override
public void leaveAst(Aast ast)
{
WorkArea wa = context.get(false);
if (wa != null && wa.history != null)
{
((Library) getLibrary()).setReportMode(true);
}
}
/**
* Helper method which records information about a name conversion.
* The type of conversion, original name, resulting name, and token
* type of the AST from which the name originated are written to a
* formatted string. The string is stored as a history entry in a list
* within the history report map. The list is indexed by the filename
* of the AST currently being processed.
*
* @param converter
* Object which performed the name conversion.
* @param result
* The name resulting from the conversion.
* @param detail
* Additional notes.
*/
private void recordHistory(NameConverter converter, String result, String detail)
{
WorkArea wa = context.get();
if (wa.history == null)
{
return;
}
if (detail == null)
{
detail = "";
}
Aast ast = getResolver().getSourceAst();
String filename = ast.getFilename().toLowerCase();
List<String> list = wa.history.get(filename);
Object[] args =
{
ast.toString(),
converter.describeConversion(),
result,
converter.getName(),
detail,
};
list.add(String.format(RPT_FMT, args));
}
/**
* Class whose instance is actually registered with the pattern engine's
* symbol resolver as the primary library of name conversion user
* functions.
*/
public class Library
{
/**
* Check if this simple Java name can collide with one of the simple names of the FWD classes which can
* be part of the imports emitted during conversion.
*
* @param name
* The simple java name.
*
* @return See above.
*/
public boolean coreSimpleNameConflict(String name)
{
return NameConverter.coreSimpleNameConflict(name);
}
/**
* Reset the reserved keyword exclusion list.
*
* Releases the old list and loads the keyword lists according to dialects specified in the
* configuration file for current {@code schema}. The {@link NameConverter} will lookup in
* {@code p2j.cfg.xml} for the schema name and use the {@code getReservedKeywords()} static
* method of each defined {@link com.goldencode.p2j.persist.dialect.Dialect} to create
* the keyword exclusion list for each schema.
*
* This method must be called for each processed database because each one can have its
* own list of dialects, otherwise the generated schema won't be compatible with respective
* dialects or the {@link NameConverter} will impose unneeded constraints to identifiers
* that matched keywords from other dialects.
*
* @param schema
* The schema whose dialects are used for configuring the keyword compatibility
* list.
*/
public void resetDialects(String schema)
{
NameConverter.resetDialects(schema);
}
/**
* Determine if the given name can be used as a Java identifier. If it can be used, determine if the
* name is not also a keyword conflict.
*
* @param name
* The name to check.
*
* @return <code>true</code> if the name can be used as a Java identifier.
*/
public boolean isValidJavaIdentifier(String name)
{
for (int i = 0; i < name.length(); i++)
{
char c = name.charAt(i);
if (!((i == 0 && Character.isJavaIdentifierStart(c)) ||
(i > 0 && Character.isJavaIdentifierPart(c))))
{
return false;
}
}
return name.equals(NameConverter.resolvePossibleKeywordConflict(name, TYPE_METHOD));
}
/**
* Convert the given name using the specified conversion type and using
* all defaults.
*
* @param name
* Symbol name to be converted.
* @param type
* Type of symbol conversion to be applied to
* <code>name</code>. Must be one of the {@link
* MatchPhraseConstants} constants.
*
* @return The converted symbol name.
*/
public String convert(String name, int type)
{
return convert(name, type, null, (Map<String, String>) null);
}
/**
* Convert the given name using the specified conversion type. Allows
* overriding or additional match/replacement pairs to be provided.
*
* @param name
* Symbol name to be converted.
* @param type
* Type of symbol conversion to be applied to
* <code>name</code>. Must be one of the {@link
* MatchPhraseConstants} constants.
* @param detail
* Optional, additional information to association with this
* conversion in the conversion report. May be
* <code>null</code>. Ignored if not in reporting mode.
* @param pairs
* An even number of strings which represent key/value pairs.
* These pairs are used to add to or override existing match
* phrase replacement entries, including those provided by
* any hints. If empty, only the default entries and those
* provided by hints are used.
*
* @return The converted symbol name.
*/
public String convert(String name, int type, String detail, Object[] pairs)
{
if (pairs.length > 0)
{
return convert(name, type, MatchPhraseConstants.ALL, true, detail, pairs);
}
WorkArea wa = context.get();
String result = wa.defaultConverter.convert(name, type);
recordHistory(wa.defaultConverter, result, detail);
return result;
}
/**
* Convert the given name using the specified conversion type and
* phrase map. The phrases in <code>replacements</code> are in addition
* to the default conversion phrases, and may override defaults.
*
* @param name
* Symbol name to be converted.
* @param type
* Type of symbol conversion to be applied to {@code name}. Must be one of the {@link
* MatchPhraseConstants} constants.
* @param detail
* Optional, additional information to association with this conversion in the conversion
* report. May be {@code null}. Ignored if not in reporting mode.
* @param replacements
* Replacement phrase mappings, where the key is the match text, and the value is the
* replacement text. If {@code null} or empty, only the default phrases are used.
*
* @return The converted symbol name.
*/
public String convert(String name, int type, String detail, Map<String, String> replacements)
{
if (type == TYPE_DENORMALIZED_PROPERTY)
{
// is a denormalized property name is requested, attempt to split the name in order to extract the
// possible index of the property from the name before processing it as a normal property. On
// return the index is added back and the underscore separator is preserved
int k = name.lastIndexOf('_');
if (k > 0 && k < name.length() - 1)
{
try
{
String idxStr = name.substring(k + 1);
Integer.parseInt(idxStr); // use the string downstream but validate that this is an integer
String rootName = name.substring(0, k);
String ret = convert(rootName, TYPE_PROPERTY, detail, replacements);
return ret + "_" + idxStr; // restore the index prefixed by the underscore separator
}
catch (NumberFormatException e)
{
// invalid number, just abort and return as expected
}
}
// downgrade to normal property
type = TYPE_PROPERTY;
}
if (type == TYPE_DATABASE &&
(SchemaDictionary.TEMP_TABLE_DB.equals(name) || SchemaDictionary.META_DB.equals(name)))
{
return name; // keep the _temp/_meta database names
}
WorkArea wa = context.get();
NameConverter converter = wa.defaultConverter;
if (replacements != null && !replacements.isEmpty())
{
converter = new NameConverter();
for (Map.Entry<String, String> next : replacements.entrySet())
{
converter.putReplacement(next.getKey(), next.getValue());
}
}
String result = converter.convert(name, type);
if (type == TYPE_TABLE) // this includes SEQUENCES, too
{
Aast ast = getResolver().getSourceAst();
String filename = ast.getFilename().toLowerCase();
Set<String> tableSet = wa.tables.get(filename);
if (tableSet.contains(result))
{
// we have a conflict. Try to use a incrementing counter to make result unique
// normally we don't expect more than one of these collisions for each name.
int k = 1;
while (tableSet.contains(result + "_" + k))
{
k++;
}
result += "_" + k;
}
// reserve it into SQL tables namespace:
tableSet.add(result);
}
recordHistory(converter, result, detail);
return result;
}
/**
* Convert the given name using the specified conversion type. Allows
* overriding or additional match/replacement pairs to be provided,
* and to select a custom set of match phrases from the current match
* phrase dictionary. Also allows replacement mode to be set.
*
* @param name
* Symbol name to be converted.
* @param type
* Type of symbol conversion to be applied to
* <code>name</code>. Must be one of the {@link
* MatchPhraseConstants} <code>TYPE_*</code> constants.
* @param phrases
* A bitwise-OR'd combination of {@link MatchPhraseConstants}
* flags indicating which set of default match phrase entries
* to load from the project-specific match phrase dictionary.
* @param replace
* If <code>true</code>, any token from the match phrase
* entries defined by <code>phrases</code> which is found
* within <code>name</code> is replaced with its designated
* replacement string. If <code>false</code>, only tokens
* from the standard entries (if <code>phrases</code>
* includes the constant <code>STANDARD</code>) are replaced.
* All other tokens are recognized as separate syllables,
* but are not replaced.
* @param detail
* Optional, additional information to association with this
* conversion in the conversion report. May be
* <code>null</code>. Ignored if not in reporting mode.
* @param pairs
* An even number of strings which represent key/value pairs.
* These pairs are used to add to or override existing match
* phrase replacement entries, including those provided by
* any hints. If empty, only the default entries and those
* provided by hints are used.
*
* @return The converted symbol name.
*/
public String convert(String name,
int type,
int phrases,
boolean replace,
String detail,
Object[] pairs)
{
if (ReservedProperty.isReservedProperty(name))
{
switch (name)
{
case Buffer.__ERROR_FLAG__: return Buffer.ERROR_FLAG_FIELD;
case Buffer.__ORIGIN_ROWID__: return Buffer.ORIGIN_ROWID_FIELD;
case Buffer.__DATA_SOURCE_ROWID__: return Buffer.DATA_SOURCE_ROWID_FIELD;
case Buffer.__ERROR_STRING__: return Buffer.ERROR_STRING_FIELD;
case Buffer.__AFTER_ROWID__: return Buffer.AFTER_ROWID_FIELD;
case Buffer.__ROW_STATE__: return Buffer.ROW_STATE_FIELD;
}
}
int len = pairs.length;
if (len % 2 != 0)
{
throw new IllegalArgumentException(
"A replacement value is missing for key: " + pairs[len - 1]);
}
NameConverter converter = new NameConverter(phrases, replace);
switch (type)
{
case TYPE_TABLE:
case TYPE_COLUMN:
case TYPE_INDEX:
converter.setSqlOverride(context.get().defaultConverter.getSqlOverride());
break;
}
for (int i = 0; i < len; i += 2)
{
converter.putReplacement((String) pairs[i], (String) pairs[i + 1]);
}
String result = converter.convert(name, type);
recordHistory(converter, result, detail);
return result;
}
/**
* Lowercase and split the input into individual package names, then ensure that each
* one is not a conflict with a Java reserved word. This will append an underscore to any
* conflicting package name.
*
* @param pkg
* A candidate Java package name, possibly including multiple period-separated
* directories.
*
* @return The reassembled and non-conflicting result.
*/
public String convertPackage(String pkg)
{
return NameConverter.convertPackage(pkg);
}
/**
* Create a (mostly) Java-bean compatible getter or setter name for a
* given field. If the field is a <code>boolean</code> and a getter is
* requested, then the prefix will be "is", otherwise getters are prefixed
* with "get". All setters are prefixed with "set". The name of the
* field always has its first character uppercased which is slightly
* different than how Sun defines the standard.
*
* @param field
* The name to use as the basis, must be non-null and non-empty.
* @param getter
* If <code>true</code> the resulting bean name will be a
* getter.
* @param bool
* If <code>getter</code> is <code>true</code> and this is
* <code>true</code> then the resulting bean name will prefixed
* with "is" instead of "get". Ignored for setters.
*
* @return The resulting getter or setter name.
*
* @throws IllegalArgumentException
* If the field name is <code>null</code> or is the empty
* string.
*/
public String generateBeanName(String field, boolean getter, boolean bool)
throws IllegalArgumentException
{
return NameConverter.generateBeanName(field, getter, bool);
}
/**
* Turn automatic name conversion report mode on or off. Enabling
* report mode simply stores name conversion history entries, but the
* report must be generated explicitly after processing is complete,
* typically within a <code>post-rule</code>. Actual report generation
* is triggered with a call to the {@link #generateReport} user
* function.
* <p>
* Report mode is <em>disabled</em> by default when this pattern worker
* is first created.
*
* @param enable
* <code>true</code> to enable report mode; <code>false</code>
* to disable it.
*/
public void setReportMode(boolean enable)
{
context.get().history = enable ? new LinkedHashMap<>() : null;
}
/**
* Set the constant describing the active SQL name conversion override.
*
* @param sqlOverride
* Constant for the desired SQL name conversion override.
*
* @see MatchPhraseConstants
*/
public void setSqlOverride(int sqlOverride)
{
context.get().defaultConverter.setSqlOverride(sqlOverride);
}
/**
* Generate a name conversion report and write it to the specified
* file. A report is only generated if report mode was enabled with
* the {@link #setReportMode} user function previous to name
* conversion. All names converted from the time report mode was
* enabled to the time this method is called are included in the
* report.
*
* @param filename
* Report destination filename. If not an absolute path, it
* is considered relative to the working directory (i.e.,
* no P2J_HOME normalization is performed).
*
* @return <code>true</code> if report mode is enabled, else
* <code>false</code>.
*
* @throws IOException
* if any I/O errors occur while persisting the report.
*/
public boolean generateReport(String filename)
throws IOException
{
WorkArea wa = context.get();
if (wa.history == null)
{
return false;
}
BufferedWriter bw = null;
try
{
bw = new BufferedWriter(new FileWriter(filename));
for (String key : wa.history.keySet())
{
List<String> list = wa.history.get(key);
// Filename.
bw.write(key);
bw.write(":");
bw.newLine();
bw.newLine();
// Headings.
Object[] args =
{
StringHelper.centerText("TOKEN", 17),
StringHelper.centerText("TYPE", 16),
StringHelper.centerText("CONVERTED", 28),
StringHelper.centerText("ORIGINAL", 16),
StringHelper.centerText("NOTES", 16),
};
bw.write(String.format(RPT_FMT, args));
bw.newLine();
String sep16 = StringHelper.repeatChar('-', 16);
String sep28 = StringHelper.repeatChar('-', 28);
String sep17 = StringHelper.repeatChar('-', 17);
args[0] = sep17;
args[1] = sep16;
args[2] = sep28;
args[3] = sep16;
args[4] = sep16;
bw.write(String.format(RPT_FMT, args));
bw.newLine();
// Content.
for (String line : list)
{
bw.write(line);
bw.newLine();
}
bw.newLine();
}
}
finally
{
if (bw != null)
{
bw.close();
}
}
return true;
}
}
/**
* Context local work area.
*/
private static class WorkArea
{
/** The default converter. */
private NameConverter defaultConverter = new NameConverter();
/** History lists of name conversions, indexed by AST filename */
private Map<String, List<String>> history = null;
/**
* Keeps the set of SQL table (and sequence) names for each schema. This is useful for checking
* whether a converted name is not already 'taken' within the same schema. Although this should
* not happen within the table name space (their names are unique in P4GL so they will be also
* unique in SQL), when adding sequences, collisions are possible. This is because in P4GL they
* have a dedicated namespace, but in SQL they share it with normal tables.
*/
private final Map<String, Set<String>> tables = new HashMap<>();
}
}