NameConverter.java
/*
** Module : NameConverter.java
** Abstract : Converts Progress symbols to Java/SQL-compatible symbols.
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20050418 @20753 Created initial version. Converts Progress
** symbol names to the equivalent Java variable,
** method, constant, or class name, or to the
** equivalent SQL database, table, column, or
** index name. Defines default substring
** replacements and naming conventions which can
** be overridden.
** 002 ECF 20050427 @20900 Better handling for multi-word replacements.
** Each word of the replacement is stored in a
** stack of pending replacements. The nextToken
** method returns one word at a time as a
** syllable. This ensures that the specialized
** syllable processing methods behave
** consistently for multi-word replacement text.
** 003 ECF 20050427 @20931 Enhancements. We now statically load a
** dictionary of default match phrases and their
** replacements. At construction, a user-defined
** subset of these entries are copied to the new
** instance. Non-standard replacements can
** optionally be disabled.
** 004 ECF 20050429 @20969 Fix for no-replace mode. When tokenizing but
** not replacing tokens, it is necessary to do a
** minimal sweep for illegal characters. The set
** of standard replacements is used for this
** purpose.
** 005 ECF 20050608 @21448 Changed NameConverter(String name, int type)
** constructor default behavior. Now replacement
** is enabled by default.
** 006 ECF 20050629 @21595 Made a NameConverter instance reusable. This
** required moving the name and type arguments
** from the constructors to the convert method,
** and making the convert method synchronized.
** This removes a huge performance penalty
** overhead in cases where the same match phrase
** entries are used across many conversions,
** since these entries can be loaded once rather
** than for every conversion.
** 007 ECF 20050719 @21772 Fix to nextToken method. Algorithm attempted
** to match candidate strings which were too
** long. Now candidates which are longer than
** the remaining symbol text are eliminated as
** match candidates. This avoids partial matches
** from appearing in the output.
** 008 GES 20051028 @23141 Added generateBeanName().
** 009 ECF 20060114 @23915 Moved generateBeanName() to StringHelper.
** Method is needed at runtime; this class is
** intended for conversion use only.
** 010 GES 20060201 @24175 Detect and resolve conflicts of Java names
** with reserved Java keywords.
** 011 GES 20060310 @24972 Support a forced full text replacement mode
** which provides a way to limit the impact of
** a common match string if the replacement
** can be forced only in the case that there is
** a match with the entire name.
** 012 GES 20060311 @24975 Added load/reload from a named match list.
** 013 ECF 20060331 @25331 Fix to #012 (@24975). Recursive design of
** convert method was not truly recursive, as
** each invocation was made on a new instance of
** the class.
** 014 ECF 20061020 @30589 Fixed the convert() method. The syllable
** counter must not advance for an empty
** syllable.
** 015 ECF 20070304 @32264 Respect camelCase as a syllable delimiter
** when converting a name. This takes precedence
** over other syllable tokenizing, since it
** represents an explicit intent by the name's
** original author.
** 016 ECF 20070305 @32267 Fix to camelCase preprocessing. Was not
** handling blocks of capital letters properly.
** 017 ECF 20071219 @36501 Fixed private convert() implementation. When
** called recursively, this method must not
** attempt to resolve possible Java keyword
** conflicts. This resolution is only done for
** the fully converted text, not per syllable.
** 018 GES 20090424 @41930 Import change.
** 019 SVL 20090709 @43129 Added keyword "count". Name conflicts are
** resolved by postpending an underscore (instead of
** prepending it).
** 020 GES 20091117 @44419 Moved the standard symbol replacements into this
** file and modified the loading mechanism to allow
** database-specific defaults to be loaded
** automatically.
** 021 CA 20130308 Convert dots in names to the "dot" string.
** 022 ECF 20131010 Separated Java and SQL reserved words, added some SQL keywords.
** 023 GES 20140118 Added convertPackage() static method.
** 024 ECF 20140923 Added SQL reserved keyword.
** 025 ECF 20141103 Added all H2 reserved keywords.
** 026 GES 20141126 Convert hyphens to underscores in pkg names.
** 027 OM 20150522 Added resetDialects() static API that allows each schema to have each
** keyword exclusion list. Generified collections.
** 028 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 029 OM 20160905 Small optimizations.
** 030 OM 20170207 Added TYPE_PROPERTY for special case of DMO property names.
** 031 ECF 20170816 Added 'assert' as a reserved java name.
** 032 OM 20170823 Updated reserved Java keyword list.
** 033 GES 20180113 In (all?) converted names, the first syllable cannot start with a
** numeric digit.
** ECF 20180114 Fixed typo.
** GES 20180131 Exclude internal calls from leading digit processing.
** 034 GES 20180905 Added replacements for other characters that can appear in malformed
** symbols. Added cleanJavaIdentifier() to handle illegal characters
** in Java identfiers.
** 035 CA 20190508 Fixed convertPackage for builtin classes.
** 036 AIL 20191015 Added object as reserved keyword, as it caused HibernateException.
** 037 CA 20200324 Converted Java method names must not collide with FWD static imported
** methods (BlockManager is included for now).
** 038 CA 20200427 A 4GL package starting with a digit will be prefixed with an
** underscore in FWD.
** CA 20200503 Added more FWD classes for static imports, to avoid collisions with
** converted method names.
** CA 20200514 Avoid collisions with java.lang.Object inherited methods.
** 039 ECF 20200906 New ORM implementation.
** 040 CA 20201015 Replaced java.util.Stack with a non-synchronized custom implementation.
** CA 20210113 Added method to check if a method's Java name is used by conversion rules to emit
** it as an unreferenced static method call. This is required to sanitize the
** implemented skeleton's method names.
** OM 20201231 Improved list of reserved SQL keywords.
** OM 20210121 Tests against reservedSQL in lowercase because SQL is case insensitive.
** OM 20210203 Avoid collision of buffer name and class with SQL keywords and java.lang classes.
** CA 20220210 Added reservedFWD, which contains the BaseDataType types.
** CA 20220621 Added p2j.ui.Element class name as reserved.
** CA 20220426 Added the FWD DMO annotations as reserved class names.
** CA 20220513 TransactionManager, SessionUtils FWD type names must be reserved.
** All public and protected method names from java.lang.Object must be reserved.
** ECF 20220514 Allow SQL name conversion in two new modes: with minimal change (resolving
** illegal characters and keyword conflicts only); and verbatim (no change at all,
** with the name stored in double quotes). TODO: verbatim mode probably will cause
** problems with DMO property annotations.
** CA 20220712 'TransactionType' and 'ResourceType' are reserved FWD class names.
** OM 20230125 Avoid collisions of index names with SQL reserved keywords.
** 041 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 042 CA 20241119 A DMO alias must not emit as a FQL reserved keyword.
** 043 DDF 20250317 Made 'Agent' a reserved FWD keyword.
** 044 CA 20250319 Added 'StringHelper' and 'ConnectionManager' as reserved names.
** 045 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.
** 'Agent' keyword is no longer reserved.
** 046 CA 20250422 Fixed H045 - for some reason, if the same package exists in multiple jars, if a
** lambda is used for the scanner, Reflections API will block. An inner class fixes
** this.
** CA 20250423 'Test' and 'Fixture' are reserved names in FWD.
*/
/*
** 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.beans.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.function.Predicate;
import java.util.logging.*;
import org.reflections.*;
import org.reflections.scanners.*;
import com.goldencode.util.Stack;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.cfg.Configuration;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.annotation.*;
import com.goldencode.p2j.persist.annotation.Trigger;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.meta.*;
import com.goldencode.p2j.persist.orm.FQLLexer;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.testengine.api.Fixture;
import com.goldencode.p2j.testengine.api.Test;
import com.goldencode.p2j.ui.Element;
import com.goldencode.p2j.ui.LogicalTerminal;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;
/**
* Converter to generate a Java or SQL symbol name from a Progress symbol
* (variable, function, procedure, filename, or schema construct). A
* <code>NameConverter</code> instance is unaware of the type of symbol
* being converted, but it is aware of the type of symbol which must result
* from the conversion. The latter is specified during construction using
* one of this class' type constants.
* <p>
* Sun-like naming conventions are used to generate Java variable, method,
* "constant", and class names:
* <ul>
* <li>variable and method names are "camel"-cased, with the first letter
* always lower case (override {@link #processVariableSyllable} and
* {@link #processMethodSyllable}, respectively, to modify);
* <li>"constant" names are all upper case, with syllables separated by an
* an underscore (override {@link #processConstantSyllable} to modify);
* <li>class names are "camel"-cased, with the first letter always upper
* case (override {@link #processClassSyllable} to modify).
* <li>conflicts with reserved Java keywords are resolved
* </ul>
* The following naming conventions are used for SQL constructs:
* <ul>
* <li>database names are all lower case, with syllables separated by an
* underscore (override {@link #processDatabaseSyllable} to modify);
* <li>table names are all lower case, with syllables separated by an
* underscore (override {@link #processTableSyllable} to modify);
* <li>column names are all lower case, with syllables separated by an
* underscore (override {@link #processColumnSyllable} to modify);
* <li>index names are all lower case, with syllables separated by an
* underscore (override {@link #processIndexSyllable} to modify).
* </ul>
*
* The converter implementation includes a generic substring replacement
* facility to permit custom naming conventions and to handle the fact that
* Java and SQL symbols supported a narrower character set within their
* respective symbol names. All replacement definitions are loaded from a
* match phrase dictionary, which is project-specific.
* <p>
* Replacement entries can be overridden as a whole via the {@link
* #setReplacements} method. Individual replacements can be overridden, and
* arbitrary replacements added, via the {@link #putReplacement} method.
* If more than one substring key matches within a symbol name, the following
* precedence rules apply:
* <ul>
* <li>a match beginning earlier in the symbol string (left to right) is
* always preferred over an overlapping match beginning later in the
* symbol string;
* <li>given two overlapping matches beginning at the same location in the
* symbol string, the more specific (i.e., longer) match string is always
* preferred over the less specific (i.e., shorter) match string.
* </ul>
*
* Multi-word replacement strings are handled such that each word is treated
* as a separate syllable in the output string.
* <p>
* Except for the type-specific rules described above, case is generally
* preserved in the converted symbol name. However, substring matches for
* the purpose of performing replacements are case-insensitive.
* <p>
* TODO: conversion to Java variable and method names should match the
* behavior of java.bean.Introspector.decapitalize(). Currently, it does so
* in 99.9% of cases, but it seems that certain character combinations are
* handled differently, such as when underscore follows a first, capital
* letter.
* <p>
* TODO: if the generic replacement functionality in this class is useful
* in other contexts, this functionality should be abstracted into a parent
* class (e.g., StringConverter). The key methods which would be moved into
* this parent would be:
* <ul>
* <li>{@link #setReplacements} - no changes
* <li>{@link #putReplacement} - no changes
* <li>{@link #convert} - no changes
* <li>{@link #nextToken} - no changes
* <li>{@link #processSyllable} - would return syllable text unmodified by
* default; subclasses would override.
* <li>{@link #prepareCandidates} - no changes
* </ul>
* All of the instance and class variables related to conversion state would
* also move there. In addition to a no-arg default constructor, a convenience
* constructor would require a map of replacements as its argument.
*/
public class NameConverter
implements MatchPhraseConstants
{
/** Used when matching substrings within symbol; indicates no match */
private static final int NO_MATCH = 0;
/** Used when matching substrings within symbol; indicates partial match */
private static final int PARTIAL_MATCH = 1;
/** Used when matching substrings within symbol; indicates full match */
private static final int FULL_MATCH = 2;
/** The map of replacements used for minimal SQL name conversion */
private static final Map<String, String> sqlMinimalReplacements;
/** Maps digit characters to safe replacement digit names. */
private static final Map<Character, String> digits = new HashMap<>();
/** A set of Java methods which can be imported static. */
private static final Set<String> staticImportedMethods = new HashSet<>();
/** A set of Java classes which are imported by default (from package {@code java.lang}). */
private static final Set<String> defaultImportedClasses = new HashSet<>();
/** The set of methods inherited from Java's {@link java.lang.Object} root class. */
private static final Set<String> objectMethods = new HashSet<>();
/** Default replacement mappings. */
private static Map<String, String> mappings = null;
/** Default substring replacements, indexed by substring to be replaced */
private static MatchPhraseDictionary defaults = null;
/** Logger */
private static final ConversionStatus LOG = ConversionStatus.get(NameConverter.class);
/** The list of imports emitted during conversion, via {@link ConverterHelper#createImport}. */
private static final String[] FWD_CORE_IMPORTS = new String[]
{
"com.goldencode.p2j.comauto.*",
"com.goldencode.p2j.email.*",
"com.goldencode.p2j.extension.*",
"com.goldencode.p2j.oo.lang.*",
"com.goldencode.p2j.persist.*",
"com.goldencode.p2j.persist.lock.*",
"com.goldencode.p2j.persist.trigger.*",
"com.goldencode.p2j.reporting.*",
"com.goldencode.p2j.security.*",
"com.goldencode.p2j.testengine.api.*",
"com.goldencode.p2j.uast.ProgressAst",
"com.goldencode.p2j.ui.*",
"com.goldencode.p2j.util.*",
"com.goldencode.p2j.xml.*",
"com.goldencode.util.*"
};
/** The list of all simple Java class names from the core imported packages, during conversion. */
private static final Set<String> FWD_CORE_SIMPLE_NAMES = new HashSet<>();
// get all (direct) classes in common packages:
static
{
for (String s : FWD_CORE_IMPORTS)
{
if (s.endsWith(".*"))
{
s = s.substring(0, s.length() - ".*".length());
Reflections r = new Reflections(s, Scanners.SubTypes.filterResultsBy(new Predicate<String>()
{
@Override
public boolean test(String t)
{
return true;
}
}));
Set<Class<?>> types = r.getSubTypesOf(Object.class);
for (Class<?> c : types)
{
if (c.getEnclosingClass() != null)
{
// skip inner classes
continue;
}
String name = c.getName();
if (c.getPackage().getName().equals(s))
{
FWD_CORE_SIMPLE_NAMES.add(c.getSimpleName());
}
}
}
else
{
FWD_CORE_SIMPLE_NAMES.add(s.substring(s.lastIndexOf('.') + 1));
}
}
}
// Set up default substring replacements
static
{
// TODO: provide a way to honor I18N here
digits.put('0', "zero");
digits.put('1', "one");
digits.put('2', "two");
digits.put('3', "three");
digits.put('4', "four");
digits.put('5', "five");
digits.put('6', "six");
digits.put('7', "seven");
digits.put('8', "eight");
digits.put('9', "nine");
try
{
mappings = new HashMap<>();
// Implement the default standard mappings (this may get overridden
// by subsequent user-defined configuration files).
mappings.put("!", "exclaim");
mappings.put("@", "at");
mappings.put("#", "number");
mappings.put("$", "dollar");
mappings.put("%", "percent");
mappings.put("^", "caret");
mappings.put("&", "and");
mappings.put("*", "asterisk");
mappings.put("+", "plus");
mappings.put("-", "");
mappings.put("_", "");
mappings.put("/", "");
mappings.put(".", "dot");
defaults = MatchPhraseDictionary.load(false, mappings);
sqlMinimalReplacements = new HashMap<>(mappings);
sqlMinimalReplacements.put("-", "_");
sqlMinimalReplacements.put("/", "_");
sqlMinimalReplacements.remove("_");
}
catch (Exception exc)
{
throw new RuntimeException("Error", exc);
}
}
/** All Java language reserved keywords or named literals */
private static final Set<String> reservedJava = new HashSet<>();
/** Keywords which are reserved in FWD - this includes unqualified FWD types emitted in converted code. */
private static final Set<String> reservedFWD = new HashSet<>();
/** Problematic SQL keywords */
private static final Set<String> reservedSQL = new HashSet<>();
/** Currently processed database name */
private static String currentSchemaName = null;
static
{
String[] reservedNames = new String[]
{
"abstract",
"assert", // added in J2SE 1.4
"boolean",
"break",
"byte",
"case",
"catch",
"char",
"class",
"const", // reserved but not used
"continue",
"default",
"do",
"double",
"else",
"enum", // added in J2SE 5.0
"extends",
"final",
"finally",
"float",
"for",
"goto", // reserved but not used
"if",
"implements",
"import",
"instanceof",
"int",
"interface",
"long",
"native",
"new",
"object",
"package",
"private",
"protected",
"public",
"return",
"short",
"static",
"strictfp", // added in J2SE 1.2
"super",
"switch",
"synchronized",
"this",
"throw",
"throws",
"transient",
"try",
"void",
"volatile",
"while",
"true", // boolean literal value
"false", // boolean literal value
"null", // null reference literal value.
};
reservedJava.addAll(Arrays.asList(reservedNames));
// add reserved FWD types. currently this includes BaseDataType sub-types
for (BaseDataType.Type type : BaseDataType.Type.values())
{
reservedFWD.add(type.getCls().getSimpleName());
}
// add other types
reservedFWD.add(Element.class.getSimpleName());
reservedFWD.add(BlockManager.TransactionType.class.getSimpleName());
reservedFWD.add(ResourceType.class.getSimpleName());
// add DMO annotations
reservedFWD.add(Index.class.getSimpleName());
reservedFWD.add(IndexComponent.class.getSimpleName());
reservedFWD.add(Indices.class.getSimpleName());
reservedFWD.add(Property.class.getSimpleName());
reservedFWD.add(Relation.class.getSimpleName());
reservedFWD.add(RelationComponent.class.getSimpleName());
reservedFWD.add(Relations.class.getSimpleName());
reservedFWD.add(Sequence.class.getSimpleName());
reservedFWD.add(Table.class.getSimpleName());
reservedFWD.add(Trigger.class.getSimpleName());
reservedFWD.add(Triggers.class.getSimpleName());
// add other FWD classes
reservedFWD.add(TransactionManager.class.getSimpleName());
reservedFWD.add(SessionUtils.class.getSimpleName());
reservedFWD.add(ConnectionManager.class.getSimpleName());
reservedFWD.add(StringHelper.class.getSimpleName());
reservedFWD.add(Test.class.getSimpleName());
reservedFWD.add(Fixture.class.getSimpleName());
// WARNING: Adding other classes/interfaces to this list may invalidated the already implemented
// skeleton classes, as these must not collide with statically emitted APIs during conversion.
Class<?>[] staticImportClasses =
{
BlockManager.class,
Enum.class,
LogicalTerminal.class,
TextOps.class,
ParameterOption.class
};
for (Class<?> cls : staticImportClasses)
{
Method[] mthds = cls.getDeclaredMethods();
for (Method mthd : mthds)
{
int mods = mthd.getModifiers();
if (Modifier.isPublic(mods) && Modifier.isStatic(mods))
{
staticImportedMethods.add(mthd.getName());
}
}
}
Method[] mthds = Object.class.getDeclaredMethods();
for (Method mthd : mthds)
{
int mods = mthd.getModifiers();
if (Modifier.isPublic(mods) || Modifier.isProtected(mods) )
{
objectMethods.add(mthd.getName());
}
}
}
/** A stack of pending replacement syllables detected via lookahead */
private final Stack<String> pendingStack = new Stack<>();
/** Map of substring replacements, indexed by substring to be replaced */
private Map<String, String> replacements = null;
/** Map of standard replacements, applied in case of no-replace mode */
private Map<String, String> standards = null;
/** Map of "full text only" replacements. */
private Map<String, String> fulltxt = null;
/** Array of candidates when matching substrings for replacement */
private String[] candidates = null;
/** Array of match state for each candidate during a match operation */
private int[] matches = null;
/** Original name to be converted */
private String name = null;
/** SQL name conversion override mode, if any */
private int sqlOverride = SQL_MODE_DEFAULT;
/** Type of conversion being performed (using this class' constants) */
private int type = 0;
/** Current position in original name symbol */
private int index = 0;
/** A substring match detected via lookahead while processing next token */
private String pendingMatch = null;
/**
* Default constructor; <code>ALL</code> default match phrases from the
* project-specific match phrase dictionary are loaded, and replacement
* is enabled.
*/
public NameConverter()
{
this(ALL, true);
}
/**
* Constructor which accepts a code indicating which default matches to
* pick up from the project's match phrase dictionary. The caller also
* chooses whether matched tokens are replaced or left in place.
*
* @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.
*/
public NameConverter(int phrases, boolean replace)
{
if (replace)
{
// get the requested replacement entries from the default match phrase dictionary.
replacements = defaults.getEntries(phrases);
}
else
{
// We will need these to sweep tokens for minimal, standard
// replacements. This ensures we don't end up with illegal
// characters in the output string.
standards = defaults.getEntries(STANDARD);
// Copy only standard replacement entries (if included in phrases
// parameter) into our local replacement map.
if ((phrases & STANDARD) != 0)
{
replacements = defaults.getEntries(STANDARD);
}
else
{
replacements = new HashMap<>();
}
// For all non-standard entries required by the phrases parameter,
// copy the match substrings as both the replacement key and value.
// Any token detected will be treated as a separate syllable and
// will be replaced with itself.
int nonStandard = (phrases & ~STANDARD);
Map<String, String> map = defaults.getEntries(nonStandard);
for (String key : map.keySet())
{
replacements.put(key, key);
}
}
// if space is not explicitly handled already, add a replacement for it.
if (!replacements.containsKey(" "))
{
replacements.put(" ", "");
}
// Load the full text list.
fulltxt = defaults.getFullText();
}
/**
* Check if the given Java method name is used as a static method call emitted by the conversion rules.
*
* @param javaname
* The Java method name.
*
* @return <code>true</code> if this is part of {@link #staticImportedMethods} or {@link #objectMethods}.
* Note that some method names (like "invoke") are not emitted statically by the conversion rules,
* and are explicitly handled.
*/
public static boolean isInternalMethodName(String javaname)
{
// this is a list of method names which are not emitted during conversion, as unreferenced static method
// calls.
String[] notStaticFWDApis =
{
"invoke",
"debug",
"getClient",
"getUserName",
"initialize",
"isEmpty",
"validate"
};
Set<String> s = new HashSet<>(Arrays.asList(notStaticFWDApis));
if (s.contains(javaname))
{
return false;
}
return staticImportedMethods.contains(javaname) || objectMethods.contains(javaname);
}
/**
* Check if this simple Java name can collide with one of the {@link #FWD_CORE_SIMPLE_NAMES} resolved from
* {@link #FWD_CORE_IMPORTS}.
*
* @param name
* The simple java name.
*
* @return See above.
*/
public static boolean coreSimpleNameConflict(String name)
{
return name != null && !name.isEmpty() && FWD_CORE_SIMPLE_NAMES.contains(name);
}
/**
* Reset the internal keyword exclusion list for the next schema to be processed.
*
* The method looks for the list of dialect used for generation of the schema and computes
* the "least common denominator". This list is obtained by merging the ANSI SQL standard
* keywords list with the list of each declared dialect.
*
* @param schema
* The schema to prepare for.
*/
public static void resetDialects(String schema)
{
if (schema.equals(currentSchemaName))
{
// already prepared for this database (this will happen only for _temp database, this
// method will be called for each .p.schema file that contain temp-tables but we don't
// need to reset the exclusion list because we are handling the same database)
return;
}
currentSchemaName = schema;
// reset the old content
reservedSQL.clear();
// ANSI SQL reserved keywords are always on the exclusion list
String[] ansiKeywords = {
"by",
"count",
"cross",
"current_date",
"current_time",
"current_timestamp",
"distinct",
"except",
"exists",
"false",
"fetch",
"for",
"from",
"full",
"group",
"having",
"inner",
"intersect",
"is",
"join",
"like",
"limit",
"minus",
"natural",
"not",
"null",
"offset",
"on",
"order",
"primary",
"rownum",
"select",
"sysdate",
"systime",
"systimestamp",
"today",
"true",
"union",
"unique",
"where",
};
reservedSQL.addAll(Arrays.asList(ansiKeywords));
String[] dialects = null;
String dialectList = null;
if (MetadataManager.META_SCHEMA.equals(schema))
{
SchemaConfig.Metadata md = Configuration.getSchemaConfig().getMetadata();
if (md != null)
{
schema = md.getName();
}
}
if (!DatabaseManager.TEMP_TABLE_SCHEMA.equals(schema))
{
// the getNamespaceParameter() can potentially throw NPE if the schema is not
// defined in as a namespace in the config file
// TODO: leave this NPE propagate or should getNamespaceParameter() return null instead?
dialectList = Configuration.getSchemaConfig().getNamespaceParameter(schema, "ddl-dialects");
}
if (dialectList == null)
{
// no dialects declared for this database/schema (_meta & _temp) databases?
// add automatically h2 dialect
dialects = new String[] { "h2" };
}
else
{
dialects = dialectList.split(",");
}
for (String dialectId : dialects)
{
Class<? extends Dialect> dialect = DialectHelper.getDialectClass(dialectId);
if (dialect != null)
{
try
{
Method m = dialect.getDeclaredMethod("getReservedKeywords");
Collection<String> keyList = (Collection<String>) m.invoke(null);
reservedSQL.addAll(keyList);
}
catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e)
{
LOG.log(Level.WARNING, "'getReservedKeywords()' method is NOT defined in " + dialect);
}
}
else
{
LOG.log(Level.WARNING, "Unknown dialect '" + dialectId + "' for '" + schema + "' schema.");
}
}
}
/**
* Load or reload the match phrase dictionary definitions from the given
* persisted match list or the default defined for the current project.
* Any cached version of the dictionary is replaced if it has been read
* previously.
*
* @param list
* The match list to use or <code>null</code> to use the default
* match list.
* @param db
* <code>true</code> to add the database specific default mappings,
* otherwise only add the code mappings.
*
* @throws ConfigurationException
* if there is any error with the global configuration or an
* error loading the dictionary from persistence. A missing
* match list file is not fatal and <em>does not</em> trigger
* this exception, but does elicit a warning message to
* <code>stderr</code>.
*/
public static void load(String list, boolean db)
throws ConfigurationException
{
// implement the default standard mappings (this may get overridden
// by subsequent user-defined configuration files).
HashMap<String, String> std = new HashMap<>(mappings);
defaults = MatchPhraseDictionary.load(list, std);
// add database-specific mappings if requested AND only if they don't already exist.
if (db)
{
Map<String, String> map = defaults.getFullText();
// if project uses old database primary key name ("id"), make sure we convert any
// occurrences to a safe default; newer default ("recid") cannot conflict, as it is a
// reserved word in 4GL
String pkName = Configuration.getSchemaConfig().getPrimaryKeyName();
if ("id".equals(pkName) && map.get(pkName) == null)
{
defaults.putFullText("id", "identifier");
}
if (map.get("date") == null)
{
defaults.putFullText("date", "dateValue");
}
}
}
/**
* 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 static String generateBeanName(String field, boolean getter, boolean bool)
throws IllegalArgumentException
{
return StringHelper.generateBeanName(field, getter, bool);
}
/**
* Get the name being converted.
*
* @return Original name.
*/
public String getName()
{
return name;
}
/**
* Completely override the default substring replacement definitions with
* the map specified. For each entry within the map, the key should be
* the substring to be replaced, and the value should be the replacement
* substring. Keys are case-insensitive; however, case is generally
* preserved in the replacement value, though it may be overridden
* depending upon the target symbol type.
*
* @param replacements
* Map of substrings to match and the text which should replace
* them in the converted symbol name, as described above.
*/
public void setReplacements(Map<String, String> replacements)
{
this.replacements = replacements;
candidates = null;
}
/**
* Override an individual, default substring replacement definition, or
* add an arbitrary substring replacement definition to the default
* definitions.
*
* @param key
* The substring to be replaced in the original symbol name.
* The match against this key is case-insensitive.
* @param value
* The text to be substituted in the target symbol name, wherever
* <code>key</code> is found in the original. Case is generally
* preserved in the replacement value, though it may be
* overridden depending upon the target symbol type.
*/
public void putReplacement(String key, String value)
{
key = key.toLowerCase();
replacements.put(key, value);
candidates = null;
// Update standards map if necessary.
if (standards != null && standards.containsKey(key))
{
standards.put(key, value);
}
}
/**
* Perform the symbol name conversion and return the converted name.
* This process breaks the original symbol into "syllables", though
* these are not traditional, English-language syllables. A syllable
* in this context is any substring within the original symbol which
* precedes or succeeds a substring replacement, or which is itself
* the replacement text resulting from such an operation.
* <p>
* For example, the symbol <code>test-name</code> is processed as three
* syllables: <code>test</code> (precedes the replacement of
* "<code>-</code>") and <code>name</code> (succeeds the replacement of
* "<code>-</code>"). In addition, the empty string which replaces the
* hyphen character between these two syllables is itself considered as
* the second syllable for processing purposes, even though it does not
* appear in the converted result:
* <ul>
* <li>for type TYPE_VARIABLE: <code>testName</code>
* <li>for type TYPE_METHOD: <code>testName</code>
* <li>for type TYPE_CONSTANT: <code>TEST_NAME</code>
* <li>for type TYPE_CLASS: <code>TestName</code>
* <li>for type TYPE_DATABASE: <code>test_name</code>
* <li>for type TYPE_TABLE: <code>test_name</code>
* <li>for type TYPE_COLUMN: <code>test_name</code>
* <li>for type TYPE_INDEX: <code>test_name</code>
* </ul>
*
* If the starting symbol were instead <code>test%name</code>, the second
* syllable would instead be <code>Pct</code> (using the default set of
* replacements).
* <p>
* The {@link #processSyllable} method is called for each syllable
* encountered. The string returned from that method is appended to the
* target symbol. This occurs until the entire, original symbol name has
* been processed.
*
* @param name
* Name to be converted.
* @param type
* One of the type constants defined in this class. Indicates
* what type of Java or SQL symbol should result from the
* conversion.
*
* @return The fully converted symbol name.
*/
public synchronized String convert(String name, int type)
{
return convert(name, type, false);
}
/**
* 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 static String convertPackage(String pkg)
{
String p = pkg.toLowerCase();
boolean builtin = false;
if (p.startsWith("progress.") || p.startsWith("openedge."))
{
builtin = true;
pkg = pkg.substring(9);
}
else if (p.startsWith("ccs."))
{
builtin = true;
pkg = pkg.substring(4);
}
// this can be static because it doesn't access any instance data nor need any of the
// normal syllable processing
// TODO: this really should be put through the standard processing because there is no
// guarantee that the inputs are valid Java package names; we should add TYPE_PKG...
String[] pieces = pkg.toLowerCase().split("\\.");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < pieces.length; i++)
{
if (i > 0)
{
sb.append(".");
}
String tok = resolvePossibleKeywordConflict(pieces[i], TYPE_CLASS);
if (!tok.isEmpty() && Character.isDigit(tok.charAt(0)))
{
tok = "_" + tok;
}
sb.append(tok.replaceAll("-", "_"));
}
return (builtin ? "com.goldencode.p2j.oo." : "") + sb.toString();
}
/**
* Retrieve a string describing the conversion type performed by this
* converter. This is useful for reporting and debugging.
*
* @return Description of conversion type.
*/
public String describeConversion()
{
String desc = null;
switch (type)
{
case TYPE_SIMPLE:
desc = "Simple";
break;
case TYPE_VARIABLE:
desc = "Java variable";
break;
case TYPE_METHOD:
desc = "Java method";
break;
case TYPE_CONSTANT:
desc = "Java \"constant\"";
break;
case TYPE_CLASS:
desc = "Java class";
break;
case TYPE_DATABASE:
desc = "SQL database";
break;
case TYPE_TABLE:
desc = "SQL table";
break;
case TYPE_COLUMN:
desc = "SQL column";
break;
case TYPE_INDEX:
desc = "SQL index";
break;
case TYPE_PROPERTY:
desc = "DMO property";
break;
case TYPE_DMO_ALIAS:
desc = "DMO alias";
break;
}
return desc;
}
/**
* Get a string representation of this object. Reports the text being
* converted and the type of conversion performed.
*
* @return String describing this object.
*/
public String toString()
{
return name + " (" + describeConversion() + ")";
}
/**
* Process each syllable which will become a component of the converted
* symbol name. This method is called <em>after</em> all substring
* replacements have already been made. Depending upon the type of the
* target symbol, this method performs different conversions on the
* syllable text, as discussed in this class' {@link NameConverter
* description}.
*
* @param text
* Text which comprises the current syllable being processed.
* @param syllable
* Zero-based index of this syllable in the converted symbol
* name.
*
* @return The current symbol after any type-specific transformation
* has been applied.
*/
protected String processSyllable(String text, int syllable)
{
switch (type)
{
case TYPE_SIMPLE:
return text;
case TYPE_VARIABLE:
case TYPE_DMO_ALIAS:
return processVariableSyllable(text, syllable);
case TYPE_METHOD:
case TYPE_PROPERTY:
return processMethodSyllable(text, syllable);
case TYPE_CONSTANT:
return processConstantSyllable(text, syllable);
case TYPE_CLASS:
return processClassSyllable(text, syllable);
case TYPE_DATABASE:
return processDatabaseSyllable(text, syllable);
case TYPE_TABLE:
return processTableSyllable(text, syllable);
case TYPE_COLUMN:
return processColumnSyllable(text, syllable);
case TYPE_INDEX:
return processIndexSyllable(text, syllable);
default:
throw new IllegalArgumentException("Illegal type: " + type);
}
}
/**
* Process a syllable for a Java variable symbol name.
* <p>
* Default processing is to lowercase the first letter of the first
* symbol, and uppercase the first letter of all subsequent syllables.
*
* @param text
* Text of current syllable.
* @param syllable
* Zero-based index of current syllable.
*/
protected String processVariableSyllable(String text, int syllable)
{
return processVarMeth(text, syllable);
}
/**
* Process a syllable for a Java method symbol name.
* <p>
* Default processing is to lowercase the first letter of the first
* symbol, and uppercase the first letter of all subsequent syllables.
*
* @param text
* Text of current syllable.
* @param syllable
* Zero-based index of current syllable.
*/
protected String processMethodSyllable(String text, int syllable)
{
return processVarMeth(text, syllable);
}
/**
* Process a syllable for a Java "constant" symbol name.
* <p>
* Default processing is to uppercase all letters and prepend an
* underscore character to each syllable after the first.
*
* @param text
* Text of current syllable.
* @param syllable
* Zero-based index of current syllable.
*/
protected String processConstantSyllable(String text, int syllable)
{
if (text.length() == 0)
{
return text;
}
StringBuilder buf = new StringBuilder();
// Set all characters to upper case; separate syllables with an
// underscore.
if (syllable > 0)
{
buf.append('_');
}
buf.append(text.toUpperCase());
return buf.toString();
}
/**
* Process a syllable for a Java class or interface symbol name.
* <p>
* Default processing is to uppercase the first letter of every syllable.
*
* @param text
* Text of current syllable.
* @param syllable
* Zero-based index of current syllable.
*/
protected String processClassSyllable(String text, int syllable)
{
if (text.length() <= 1)
{
return text.toUpperCase();
}
// Set first character of every syllable to upper case.
return Character.toUpperCase(text.charAt(0)) + text.substring(1);
}
/**
* Process a syllable for an SQL database symbol name.
* <p>
* Default processing is to lowercase all letters and prepend an
* underscore character to each syllable after the first.
*
* @param text
* Text of current syllable.
* @param syllable
* Zero-based index of current syllable.
*/
protected String processDatabaseSyllable(String text, int syllable)
{
return processSql(text, syllable);
}
/**
* Process a syllable for an SQL table symbol name.
* <p>
* Default processing is to lowercase all letters and prepend an
* underscore character to each syllable after the first.
*
* @param text
* Text of current syllable.
* @param syllable
* Zero-based index of current syllable.
*/
protected String processTableSyllable(String text, int syllable)
{
return processSql(text, syllable);
}
/**
* Process a syllable for an SQL column symbol name.
* <p>
* Default processing is to lowercase all letters and prepend an
* underscore character to each syllable after the first.
*
* @param text
* Text of current syllable.
* @param syllable
* Zero-based index of current syllable.
*/
protected String processColumnSyllable(String text, int syllable)
{
return processSql(text, syllable);
}
/**
* Process a syllable for an SQL index symbol name.
* <p>
* Default processing is to lowercase all letters and prepend an
* underscore character to each syllable after the first.
*
* @param text
* Text of current syllable.
* @param syllable
* Zero-based index of current syllable.
*/
protected String processIndexSyllable(String text, int syllable)
{
return processSql(text, syllable);
}
/**
* Get the active SQL name conversion override mode, if any.
*
* @return Constant for the active SQL name conversion override mode.
*
* @see MatchPhraseConstants
*/
int getSqlOverride()
{
return sqlOverride;
}
/**
* Set the active SQL name conversion override mode.
*
* @param sqlOverride
* Constant for the active SQL name conversion override mode.
*
* @see MatchPhraseConstants
*/
void setSqlOverride(int sqlOverride)
{
this.sqlOverride = sqlOverride;
}
/**
* Perform the symbol name conversion and return the converted name.
* This process breaks the original symbol into "syllables", though
* these are not traditional, English-language syllables. A syllable
* in this context is any substring within the original symbol which
* precedes or succeeds a substring replacement, or which is itself
* the replacement text resulting from such an operation.
* <p>
* For example, the symbol <code>test-name</code> is processed as three
* syllables: <code>test</code> (precedes the replacement of
* "<code>-</code>") and <code>name</code> (succeeds the replacement of
* "<code>-</code>"). In addition, the empty string which replaces the
* hyphen character between these two syllables is itself considered as
* the second syllable for processing purposes, even though it does not
* appear in the converted result:
* <ul>
* <li>for type TYPE_VARIABLE: <code>testName</code>
* <li>for type TYPE_METHOD: <code>testName</code>
* <li>for type TYPE_CONSTANT: <code>TEST_NAME</code>
* <li>for type TYPE_CLASS: <code>TestName</code>
* <li>for type TYPE_DATABASE: <code>test_name</code>
* <li>for type TYPE_TABLE: <code>test_name</code>
* <li>for type TYPE_COLUMN: <code>test_name</code>
* <li>for type TYPE_INDEX: <code>test_name</code>
* <li>for type TYPE_PROPERTY: <code>testName</code> or <code>TName</code> in certain cases,
* where after a lowercase char there is a set of uppercases chars. This is done in
* order to match the way Hibernate expectes these property names.
* </ul>
*
* If the starting symbol were instead <code>test%name</code>, the second
* syllable would instead be <code>Pct</code> (using the default set of
* replacements).
* <p>
* The {@link #processSyllable} method is called for each syllable
* encountered. The string returned from that method is appended to the
* target symbol. This occurs until the entire, original symbol name has
* been processed.
* <p>
* This method may be invoked as an internal attempt to remove any illegal
* characters from a converted syllable. In this case, full text only
* mode is ignored.
*
* @param name
* Name to be converted.
* @param type
* One of the type constants defined in this class. Indicates
* what type of Java or SQL symbol should result from the
* conversion.
* @param internal
* <code>true</code> if this method is invoked due to internal
* housekeeping; <code>false</code> if invoked as the result of
* an external call.
*
* @return The fully converted symbol name.
*/
private String convert(String name, int type, boolean internal)
{
boolean preprocess = true;
this.type = type;
this.name = name;
switch (type)
{
// if a SQL name conversion override mode is active, bypass the normal name conversion logic for
// table and column names (and simple names, which will be the type when calling this method in
// a second pass for the minimal change mode)
case TYPE_SIMPLE:
if (!internal)
{
break;
}
// intentional fall through if internal
case TYPE_TABLE:
case TYPE_COLUMN:
case TYPE_INDEX:
switch (sqlOverride)
{
case SQL_MODE_MINIMAL:
if (!internal)
{
// return the name as is, modifying it only to resolve a keyword conflict or to replace
// illegal characters
return processSqlMinimal(name);
}
preprocess = false;
break;
case SQL_MODE_VERBATIM:
if (!internal)
{
// return the name as is, in double quotes
return "\"" + name + "\"";
}
break;
default:
break;
}
break;
default:
break;
}
if (preprocess)
{
this.name = preprocess(name);
}
index = 0;
// honor full text only mode, if specified for the given name AND not an internal invocation
if (!internal)
{
String result = fullTextReplacement(this.name);
if (result != null)
{
return processSyllable(result, 0);
}
}
prepareCandidates();
StringBuilder buf = new StringBuilder();
String tok = null;
for (int i = 0; (tok = nextToken()) != null; i++)
{
if (standards != null)
{
// Sweep tok to at least apply standard replacements, in order
// to prevent illegal characters in the output.
NameConverter nc = new NameConverter(0, true);
nc.setReplacements(standards);
tok = nc.convert(tok, type, true);
}
// the next two cases must be protected with the !internal check because in internal
// mode i == 0 when processing subsequent syllables; this really needs to be cleaned
// up because it breaks the contract and makes it harder to implement proper logic
// for all symbol types, the first character cannot start with a digit
if (!internal && i == 0 && tok != null && tok.length() > 0)
{
tok = processLeadingDigit(tok);
}
//
if (!internal &&
(type == TYPE_VARIABLE || type == TYPE_METHOD || type == TYPE_PROPERTY ||
type == TYPE_CONSTANT || type == TYPE_CLASS || type == TYPE_DMO_ALIAS))
{
tok = cleanJavaIdentifier(tok, i);
}
buf.append(processSyllable(tok, i));
// Adjust the syllable count downward to skip this syllable if,
// after conversion, it's empty.
if (tok == null || tok.length() == 0)
{
i--;
}
}
String result = buf.toString();
// special handling for DMO properties: in some cases, the property name does not have the
// first letter in lowercase. This quirk is necessary for compatibility with Hibernate's
// internal property name conversions.
if (type == TYPE_PROPERTY)
{
if (!result.isEmpty())
{
char[] chars = result.toCharArray();
chars[0] = Character.toUpperCase(chars[0]);
result = new String(chars);
}
result = Introspector.decapitalize(result);
}
// resolve possible keyword conflicts on the converted output; we don't do this when called
// recursively, because we don't want to process individual syllables, just the final result
if (!internal)
{
result = resolvePossibleKeywordConflict(result, type);
}
return result;
}
/**
* Preprocess the given name. This involves splitting the name into
* syllables which are already well established by camel casing, then
* lowercasing the entire name. Syllables are delimited by spaces in the
* preprocessed string.
*
* @param name
* Name to be converted.
*
* @return Preprocessed name, lowercased and split into syllables divined
* from camel case in the original name.
*/
private String preprocess(String name)
{
StringBuilder buf = new StringBuilder();
int len = name.length();
char last = 0;
for (int i = 0; i < len; i++)
{
char c = name.charAt(i);
if (Character.isUpperCase(c) && i > 0 && !Character.isUpperCase(last))
{
buf.append(' ');
}
buf.append(Character.toLowerCase(c));
last = c;
}
return buf.toString().trim();
}
/**
* Process a SQL name with minimal change, replacing only illegal characters and resolving keyword
* conflicts. The original case of the characters in the name is preserved.
*
* @param name
* Table or field name to be converted.
*
* @return The given name, converted with minimal change.
*/
private String processSqlMinimal(String name)
{
String result = name;
// sweep name to apply standard replacements, in order to prevent illegal characters in the output
NameConverter nc = new NameConverter(0, true);
nc.setSqlOverride(sqlOverride);
nc.setReplacements(sqlMinimalReplacements);
result = nc.convert(result, TYPE_SIMPLE, true);
result = resolvePossibleKeywordConflict(result, type);
return result;
}
/**
* If the given candidate name is marked as a forced full text match (a
* match that is only allowed with the full text of the name rather than
* as a partial basis), return that replacement.
*
* @param candidate
* The string that is to be tested.
*
* @return The full text replacement or <code>null</code> if this
* candidate is not forced into full text mode.
*/
private String fullTextReplacement(String candidate)
{
return fulltxt.get(candidate);
}
/**
* Process Java identifiers and filter out any characters that are not allowed. Illegal
* characters will be converted to a 'uXXXX' format where the 16-bit hexidecimal char
* value will be rendered as text.
*/
private String cleanJavaIdentifier(String text, int syllable)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < text.length(); i++)
{
char ch = text.charAt(i);
boolean bad = (syllable == 0 && i == 0) ? !Character.isJavaIdentifierStart(ch)
: !Character.isJavaIdentifierPart(ch);
if (bad)
{
sb.append(String.format("u%04X", (int) ch));
}
else
{
sb.append(ch);
}
}
return sb.toString();
}
/**
* This is the backing method behind the {@link #processVariableSyllable}
* and {@link #processMethodSyllable} methods, which both have the same
* default behavior.
*
* @param text
* Text of current syllable.
* @param syllable
* Zero-based index of current syllable.
*/
private String processVarMeth(String text, int syllable)
{
int len = text.length();
if (len < 1)
{
return text;
}
StringBuilder buf = new StringBuilder();
// Set first character of first syllable to lower case; first
// character of all other syllables to upper case.
if (syllable == 0)
{
buf.append(Character.toLowerCase(text.charAt(0)));
}
else
{
buf.append(Character.toUpperCase(text.charAt(0)));
}
if (len > 1)
{
buf.append(text.substring(1));
}
return buf.toString();
}
/**
* This is the backing method behind the {@link #processDatabaseSyllable},
* {@link #processTableSyllable}, {@link #processColumnSyllable}, and
* {@link #processIndexSyllable} methods, which all have the same default
* behavior.
*
* @param text
* Text of current syllable.
* @param syllable
* Zero-based index of current syllable.
*/
private String processSql(String text, int syllable)
{
if (text.length() == 0)
{
return text;
}
StringBuilder buf = new StringBuilder();
// Set all characters to lower case; separate syllables with an
// underscore.
if (syllable > 0)
{
buf.append('_');
}
buf.append(text.toLowerCase());
return buf.toString();
}
/**
* Generate the next token (aka syllable) to be appended to the target
* symbol name. A token generated by this method will either be:
* <ul>
* <li>a run of characters for which no substring replacement could be
* matched; or
* <li>the replacement text for a run of characters which was matched.
* </ul>
* Each token consists only of those characters up to the next run as
* described above, or to the end of the original symbol string for the
* last such run.
* <p>
* For example, the symbol <code>this&that</code> would generate three
* tokens (i.e., syllables), using the default replacement map:
* <ol>
* <li><code>this</code> - not matched for any substring replacement; its
* end is determined by the fact that the next character begins a
* a run of characters (in this case, one character, actually) which
* is matched to replacement key <code>&</code>;
* <li><code>and</code> - replacement text for substring
* <code>&</code>;
* <li><code>that</code> - not matched for any substring replacement; its
* end is determined by the end of the original token string.
* </ol>
*
* @return The next token, as described above, or <code>null</code> if
* we have reached the end of the original symbol string and
* we have no new characters to return.
*/
private String nextToken()
{
// If we found a full match after finding one or more non-matching
// characters in a previous pass through this method, we will have
// a pending match. Return the next syllable of its replacement now
// as the next token, advancing the index to just beyond the matched
// text.
String replacement = getPendingReplacement();
if (replacement != null)
{
// Consume the pending match.
if (pendingMatch != null)
{
index += pendingMatch.length();
pendingMatch = null;
}
return replacement;
}
resetMatchStatus();
// Create buffer for next token.
StringBuilder buf = new StringBuilder();
// Offset from beginning of match keys.
int off = 0;
// Number of full key matches at the current character from the
// original symbol name.
int full = 0;
// Number of partial key matches at the current character from the
// original symbol name.
int partial = 0;
// Index of the character on which we last started a match, in case
// the match turns out to be a bust and we have to push back lookahead
// characters.
int lastMatchStart = -1;
// Scan a character at a time of the original symbol name, starting
// at the current index, as advanced by previous calls to this method.
int len = name.length();
int i;
for (i = 0; index + i < len; i++)
{
// next character from original symbol; lowercase unless conversion type is simple.
char ch = name.charAt(index + i);
if (type != TYPE_SIMPLE)
{
ch = Character.toLowerCase(ch);
}
// Number of remaining characters in original symbol yet to be
// tested.
int remaining = len - index - i;
// Walk the list of match statuses for each key and update, based
// on whether the current character matches or not.
for (int j = 0; j < matches.length; j++)
{
String key = candidates[j];
if (matches[j] != FULL_MATCH)
{
int keyLen = key.length();
if (keyLen - off <= remaining &&
off < keyLen &&
Character.toLowerCase(key.charAt(off)) == ch)
{
if (off + 1 == keyLen)
{
// We have successfully matched the final character of
// the key. Update the key's status to a full match.
boolean fromPartial = (matches[j] == PARTIAL_MATCH);
if (fromPartial)
{
partial--;
}
// Only consider this a full match if we were previously
// a partial match or if the key is only one character
// long.
if (fromPartial || keyLen == 1)
{
matches[j] = FULL_MATCH;
full++;
}
}
else if (matches[j] == NO_MATCH && off == 0)
{
// We have successfully matched the first character
// Update the key's status to a partial match.
partial++;
matches[j] = PARTIAL_MATCH;
}
}
else if (matches[j] == PARTIAL_MATCH)
{
// We had a partial match, but the current character did
// not match. Update the key's status to no match.
matches[j] = NO_MATCH;
partial--;
}
}
}
/*
// DEBUG CODE
System.out.println("ch = " + ch);
System.out.println("candidates = " + Arrays.asList(candidates));
System.out.print("matches = [");
for (int x = 0; x < matches.length; x++)
{
if (x > 0)
System.out.print(", ");
System.out.print(String.valueOf(matches[x]));
}
System.out.println("]");
System.out.println("i = " + i);
System.out.println("index = " + index);
System.out.println("len = " + len);
System.out.println("partial = " + partial);
System.out.println("full = " + full);
System.out.println();
*/
if (partial > 0 || full > 0)
{
// Increment the key offset if any key had any match.
off++;
// Remember where this match began, in case it fails and we
// need to push back the lookahead.
if (lastMatchStart < 0)
{
lastMatchStart = i;
}
}
else if (partial == 0 && full == 0)
{
// Reset the key offset if no keys had a match.
off = 0;
// Conditionally push back.
if (lastMatchStart >= 0)
{
resetMatchStatus();
// Push back all characters to the position just after where
// we last matched.
i = lastMatchStart;
lastMatchStart = -1;
// Truncate the token buffer accordingly.
buf.setLength(i + 1);
// Skip the remainder of the outer loop, because we don't
// want to add the current character to the buffer after
// we've truncated it.
continue;
}
}
if (full > 0 && (partial == 0 || (index + i) == (len - 1)))
{
// At least one full match has been found, AND all partial
// matches have resolved to either a full match or no match
// at all OR we are at the end of the input string.
// Always prefer the most specific (i.e., longest), matching
// key.
String key = null;
for (int k = 0; k < matches.length; k++)
{
if (matches[k] == FULL_MATCH)
{
String next = candidates[k];
if (key == null || next.length() > key.length())
{
key = next;
}
}
}
// Set the pending replacement word(s) into the pending stack.
// These will be popped as replacements are returned from this
// method.
setupPendingStack(key);
if (lastMatchStart > 0)
{
// If we already found one or more non-matching characters
// before we detected this full match, we have to return the
// non-matching character(s) as the next token. However, we
// set the full match into our pendingMatch instance variable,
// so that we use it immediately on our next call into this
// method.
pendingMatch = key;
index += lastMatchStart;
buf.setLength(lastMatchStart);
return buf.toString();
}
else
{
// Our matching key was not preceded by any non-matching
// characters, so we can return the replacement text
// immediately.
index += key.length();
return getPendingReplacement();
}
}
// Add the character to the current token buffer.
buf.append(ch);
}
// Update the index to reflect all "lookahead" characters processed
// with no match.
index += i;
// Get the string of unmatched characters.
String token = buf.toString();
// Return the buffer or null if no new characters are available.
return (token.length() > 0 ? token : null);
}
/**
* Reset the list of potential matches so that each candidate has an
* equal starting state of NO_MATCH. As soon any character in a
* candidate is found to match, its corresponding entry is set to
* PARTIAL_MATCH. If all characters match, it is set to FULL_MATCH.
*/
private void resetMatchStatus()
{
Arrays.fill(matches, NO_MATCH);
}
/**
* Set up the stack of pending replacement words. Each word will be
* returned as a separate token (i.e., syllable) from {@link #nextToken}
* until the stack is empty.
*
* @param key
* A substring which was matched in the original symbol; the
* key to a replacement string in the map of replacements.
*/
private void setupPendingStack(String key)
{
String replacement = replacements.get(key);
String[] words = replacement.split(" ");
for (int i = words.length - 1; i >= 0; i--)
{
pendingStack.push(words[i]);
}
}
/**
* Get the next pending replacement string, if any, based on a previous
* substring match.
*
* @return Next pending replacement syllable, or <code>null</code> if
* the stack of pending replacements is empty.
*/
private String getPendingReplacement()
{
String replacement = null;
if (!pendingStack.isEmpty())
{
try
{
replacement = pendingStack.pop();
}
catch (EmptyStackException exc)
{
LOG.log(Level.WARNING,"", exc);
}
}
return replacement;
}
/**
* Perform preparation of the data structures used to match candidate
* substrings in the {@link #nextToken} method. This includes setting
* each candidate key string into an array, and creating a new array
* of integers to track the match status of each candidate key as the
* <code>nextToken</code> method scans characters from the original
* symbol.
*/
private void prepareCandidates()
{
if (candidates != null)
{
return;
}
Set<String> keys = replacements.keySet();
int size = keys.size();
candidates = new String[size];
matches = new int[size];
Iterator<String> iter = keys.iterator();
for (int i = 0; iter.hasNext(); i++)
{
String next = iter.next();
candidates[i] = next;
}
}
/**
* Test the given name against the list of Java keywords and resolve any
* conflict by adding text to the name to make it different (and thus
* javac will compile it). An underscore character will be prepended
* to the name to resolve any conflicts.
*
* @param possible
* The name to check for conflicts.
* @param type
* The type of name. This is used to ensure that the name is
* a Java name (and thus requires conflict resolution).
*
* @return The original name if no conflicts were detected or a
* disambiguated (safe) name if a conflict existed.
*/
public static String resolvePossibleKeywordConflict(String possible, int type)
{
String lcPossible = possible.toLowerCase();
boolean hit = false;
switch (type)
{
case TYPE_VARIABLE:
case TYPE_CONSTANT:
case TYPE_PROPERTY:
{
// only emitted class fields are checked with reserved FWD names. the fall-through to TYPE_METHOD
// is required to check the reserved Java names, too.
hit = reservedFWD.contains(possible);
}
case TYPE_METHOD:
{
hit = hit || reservedJava.contains(possible);
break;
}
case TYPE_CLASS:
hit = reservedJava.contains(lcPossible) ||
reservedFWD.contains(possible) ||
reservedSQL.contains(lcPossible);
if (!hit)
{
// exquisite search: avoid collisions with java.lang. Unfortunately,
// Reflections("java.lang").getSubTypesOf(Object.class);
// cannot be used because of security reasons (?), so we built it incrementally
hit = defaultImportedClasses.contains(possible);
if (!hit)
{
try
{
Class.forName("java.lang." + possible, false, String.class.getClassLoader());
hit = true;
defaultImportedClasses.add(possible);
}
catch (ClassNotFoundException e)
{
// this is expected: no collisions
}
}
}
break;
case TYPE_DMO_ALIAS:
hit = FQLLexer.getReserved().contains(lcPossible);
case TYPE_TABLE:
{
hit = hit ||
reservedJava.contains(possible) ||
reservedFWD.contains(possible) ||
reservedSQL.contains(lcPossible);
break;
}
case TYPE_DATABASE:
case TYPE_COLUMN:
case TYPE_INDEX:
{
// testing in lowercase because SQL is case-insensitive
hit = reservedSQL.contains(lcPossible);
break;
}
default:
break;
}
// methods used in static imports can't be used in definitions...
if (hit || (type == TYPE_METHOD && (staticImportedMethods.contains(possible) ||
objectMethods.contains(possible))))
{
possible += "_";
}
return possible;
}
/**
* Replace the leading character with safe text if it is a digit.
*
* @param text
* The syllable to process.
*
* @return The modified text if the first character was a numeric digit.
*/
private String processLeadingDigit(String text)
{
char c = text.charAt(0);
return Character.isDigit(c) ? digits.get(c) + text.substring(1) : text;
}
/**
* Entry point and test harness for this class. Expects the following
* arguments:
* <ol>
* <li><code>symbol</code> (required) - Progress symbol name to be
* converted;
* <li><code>type</code> (required) - type of conversion to apply. Use
* the integer equivalents of the <code>TYPE_*</code> constants
* defined by this class;
* <li><code>replace</code> (required) - true/false. True to replace
* substrings as the name is tokenized; false to tokenize syllables
* without replacement;
* <li><code>key</code> (optional) - substring in <code>symbol</code>
* to be replaced by the following parameter;
* <li><code>replacement</code> (required) - replacement text for the
* immediately previous parameter.
* </ol>
* Any number of key/replacement pairs may be provided. If a key is
* provided with no following replacement, that key is ignored.
*
* @param args
* Command line parameters as described above.
*/
public static void main(String[] args)
{
try
{
if (args.length < 3)
{
System.out.println("Usage: NameConverter \"symbol\" type replace phrases " +
"[\"key\", \"replacement\"]...");
System.exit(-1);
}
String name = args[0];
int type = Integer.parseInt(args[1]);
boolean replace = Boolean.parseBoolean(args[2]);
int phrases = ALL;
if (args.length > 3)
{
phrases = Integer.parseInt(args[3]);
}
NameConverter conv = new NameConverter(phrases, replace);
conv.setSqlOverride(SQL_MODE_MINIMAL);
int i;
for (i = 4; i + 1 < args.length; i += 2)
{
String key = args[i];
String rpl = args[i + 1];
conv.putReplacement(key, rpl);
}
if (i + 1 == args.length)
{
System.out.println("Extra argument '" + args[i] + "' ignored");
}
System.out.println(name + " ---> " + conv.convert(name, type) +
" (" + conv.describeConversion() + ")");
/*
long time = System.currentTimeMillis();
for (i = 0; i < 10000; i++)
{
conv.convert(name, type);
}
time = System.currentTimeMillis() - time;
System.out.println("Elapsed: " + time);
*/
}
catch (Exception exc)
{
LOG.log(Level.WARNING,"", exc);
}
}
}