NameNode.java
/*
** Module : NameNode.java
** Abstract : Node in a progress schema namespace
**
** Copyright (c) 2004-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20041217 @19170 Created initial version.
** 002 ECF 20040118 @19340 Added getParent method for use by
** SchemaDictionary.
** 003 ECF 20050210 @19744 Added toAliasedName method for use by
** SchemaDictionary. Modified toQualfiedName
** to delegate to this new method.
** 004 ECF 20050216 @19839 Added setParent method to allow parent to be
** changed. Automatically removes this node from
** old parent's namespace before adding it to
** new parent's namespace.
** 005 ECF 20050308 @20246 Modified toAliasedName to correctly handle
** temp- and work-table names. These do not have
** database qualifiers, which broke the previous
** algorithm.
** 006 ECF 20050413 @20705 Changed internal data storage mechanism to an
** AST. External interface is mostly unchanged,
** except that a different constructor must now
** be used, and there are new methods to get/set
** the backing AST. Added a convenience
** constructor to create a NameNode based only
** on a name (package private for use by the
** Namespace class).
** 007 GES 20070402 @32702 Provided access to a new flag to identify
** name nodes that should be preferred in
** ambiguous name conflicts.
** 008 ECF 20070628 @35341 Minor optimization. Replaced StringBuffer
** with StringBuilder.
** 009 GES 20090518 @42396 Import change.
** 010 ECF 20111217 Specified generics for raw types.
** 011 CA 20130524 Added getOrder(), used by temp-table field ordering fix.
** 012 ECF 20160328 Minor code/doc cleanup.
** 013 OM 20211122 Added hints-based support for 'unloading' schema.
** GES 20220404 Handle the case where there is no ORDER specified in the schema for a field.
** CA 20221006 Keep a lowercase version of the AST text. Refs #6820
** 014 OM 20230115 Replaced absolutePath(), relativePath(), upPath() and downPath() with faster
** versions, based on node types rather on string paths.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.schema;
import java.io.*;
import java.util.*;
import com.goldencode.ast.*;
import com.goldencode.p2j.uast.*;
/**
* A single node in a Progress database entity's name, which represents a
* database, a table, or a field entity. Each node holds a {@link Namespace}
* object, which may contain children nodes (only for nodes which represent
* databases and tables; field nodes have no children).
* <p>
* Nodes representing fields have a parent table node; those representing
* tables have a parent database node; database nodes represent the top of
* the hierarchy and as such, have no parent node. Thus, any node will be
* able to reconstruct its own fully qualified name by walking up the chain
* of its ancestors ({@link #toQualifiedName} does this).
* <p>
* This class defines two comparators as inner classes ({@link
* NameNode.ExactMatchComparator} and {@link NameNode.AbbreviationComparator})
* which are used by {@link Namespace} for name lookups. The former is also
* used to define the natural order of <code>NameNode</code> instances for
* sorting purposes.
* <p>
* NameNode is backed by an AST which is stored internally, and which is
* accessible via accessor methods. The token type of the backing AST defines
* the data type of the name node. The text of the AST defines the name of
* the name node.
* <p>
* <strong>Note:</strong> instances of this object are used as keys in a
* <code>WeakHashMap</code> {@link SchemaDictionary#scopeMap mapping} in the
* <code>SchemaDictionary</code>. That mapping relies on this object using
* <code>==</code> as its implementation of the <code>equals</code> method
* (inherited from <code>java.lang.Object</code>). Any other implementation
* of <code>equals</code> will break that mapping!
*/
public class NameNode
implements Comparable<NameNode>
{
/** Comparator which requires exact lexical match of node names */
public static final ExactMatchComparator EXACT_COMPARATOR = new ExactMatchComparator();
/** Comparator which allows abbreviations of node names */
public static final AbbreviationComparator ABBREVIATION_COMPARATOR = new AbbreviationComparator();
/** Optional parent associated with this name node */
private NameNode parent = null;
/** Namespace containing child nodes, if any */
private final Namespace children = new Namespace(true);
/** AST node associated with this name node */
private Aast ast = null;
/** Cache the lower version of the AST's text, to avoid lowercasing in {@link #getName()}. */
private String astLowerText = null;
/**
* Constructor which specifies node's parent, AST, and name.
*
* @param parent
* Optional parent name node. If not <code>null</code> this node
* will be added to <code>parent</code>'s namespace.
* @param ast
* AST node to be associated with this name node.
*
* @throws NullPointerException
* if <code>name</code> is <code>null</code>.
*/
public NameNode(NameNode parent, Aast ast)
{
this.parent = parent;
setAst(ast);
setParent(parent);
}
/**
* Convenience constructor to create a temporary name node.
*
* @param name
* Name assigned to the name node. May not be <code>null</code>.
*/
NameNode(String name)
{
ast = new ProgressAst();
ast.setText(name.toLowerCase());
astLowerText = ast.getText();
}
/**
* Get the parent, if any, associated with this node.
*
* @return Parent node, may be <code>null</code>.
*/
public NameNode getParent()
{
return parent;
}
/**
* Set the parent name node of this node. If we had an existing parent,
* remove this node from its private namespace. If <code>parent</code> is
* not <code>null</code>, add this node to its private namespace.
*
* @param parent
* New parent name node.
*/
public void setParent(NameNode parent)
{
// Remove ourselves from existing parent's private namespace.
if (this.parent != null)
{
this.parent.getNamespace().remove(this);
}
// Add ourselves to new parent's private namespace.
if (parent != null)
{
parent.getNamespace().add(this);
}
this.parent = parent;
}
/**
* Get the AST which backs this name node.
*
* @return Backing AST.
*/
public Aast getAst()
{
return ast;
}
/**
* Set the AST which backs this name node.
*
* @param ast
* Backing AST. Should not be <code>null</code>.
*/
public void setAst(Aast ast)
{
this.ast = ast;
this.astLowerText = ast == null ? null : ast.getText().toLowerCase();
}
/**
* Get the name associated with this node.
*
* @return Name associated with this node; always lower case.
*/
public String getName()
{
return astLowerText;
}
/**
* Get the data type associated with this node.
*
* @return A Progress token type representing the AST backing this node.
*/
public int getType()
{
return ast.getType();
}
/**
* Reports if this node is to be preferred in ambiguous name conflicts
* over other name nodes that do not have this flag set.
*
* @return The preferred flag.
*/
public boolean isPreferred()
{
boolean prefer = false;
if (ast.isAnnotation("preferred"))
{
prefer = ((Boolean) ast.getAnnotation("preferred")).booleanValue();
}
return prefer;
}
/**
* Get the namespace which contains this node's children, if any.
*
* @return Namespace for this node's children.
*/
public Namespace getNamespace()
{
return children;
}
/**
* Get the fully qualified, unabbreviated name which corresponds with this
* node. For this purpose, ancestors are considered, but not descendants.
* No alias substitutions are made.
*
* @return Qualified name in the form {@code [[grandparent_name.]parent_name.]name}.
* Note that this node might not have a grandparent or even a parent, in
* which case the missing ancestor names are omitted.
*/
public String toQualifiedName()
{
StringBuilder buf = new StringBuilder(getName());
NameNode next = this;
while ((next = next.getParent()) != null)
{
buf.insert(0, '.');
buf.insert(0, next.getName());
}
return buf.toString();
}
/**
* Compares this name node with the specified name node to determine the
* natural order of name nodes. Delegates this comparison to {@link
* NameNode.ExactMatchComparator}.
*
* @param o
* <code>NameNode</code> for comparison. If <code>null</code> or
* not of type <code>NameNode</code>, <code>1</code> is returned.
*
* @return <code>0</code> if objects are considered equal by the rules
* of the {@link NameNode.ExactMatchComparator}; <code>-1</code>
* if <code>this</code> is considered less than <code>o</code>;
* <code>1</code> if <code>this</code> is considered greater than
* <code>o</code>.
*/
@Override
public int compareTo(NameNode o)
{
return EXACT_COMPARATOR.compare(this, o);
}
/**
* Returns a string representation of this name node, primarily for debug
* purposes.
*
* @return String representation of the internal state of this object.
*/
@Override
public String toString()
{
return toQualifiedName();
}
/**
* Obtain the schema of this {@code NameNode}.
*
* @return the schema component of this {@code NameNode}. Always in lowercase.
*/
public String getSchema()
{
NameNode nameNode = this;
while (nameNode.parent != null)
{
nameNode = nameNode.parent;
}
return nameNode.getName();
}
/**
* Get the fully qualified, unabbreviated name which corresponds with this
* node. For this purpose, ancestors are considered, but not descendants.
* This implementation is aware of database aliases. It will substitute the
* database name portion of <code>entityName</code>, if any, for the
* database name portion of the qualified name
*
* @param type
* Entity type, using {@link EntityName} constants.
* @param entityName
* If this object specifies a database qualifier, it is used in
* preference to the qualifier discovered by inspection of this
* node's internal state or lineage. May be <code>null</code>; if
* so, no database alias substitution is performed.
*
* @return Qualified name in the form {@code database[.table[.field]]}
* for regular schema entities, or <code>table[.field]</code> for temp
* or work tables (or their fields).
*/
String toAliasedName(int type, EntityName entityName)
{
List<String> parts = new LinkedList<>();
NameNode next = this;
// Assemble a list of name parts from this node's name and those of its
// ancestors. The list is ordered from least to most specific name part.
do
{
parts.add(0, next.getName());
next = next.getParent();
} while (next != null);
// Normalize the list: make sure we have 3 name parts, even if the
// first or last is null.
// A database, temp-table or work-table will only have one part at
// this point.
if (parts.size() == 1)
{
switch (type)
{
case EntityName.DATABASE:
parts.add(null); // add null table reference
break;
case EntityName.TABLE:
parts.add(0, null); // add null database qualifier
break;
}
}
// A database, temp-table or work-table, or a field of a temp-table or
// of a work-table will only have two parts at this point.
if (parts.size() == 2)
{
switch (type)
{
case EntityName.DATABASE:
case EntityName.TABLE:
parts.add(null); // add null field reference
break;
case EntityName.FIELD:
parts.add(0, null); // add null database qualifier
break;
}
}
// If an entity name was provided, and that name included a database
// or table part, prefer those qualifier names to the ones determined
// by this node's lineage. This will replace the real names with the
// appropriate aliases.
if (entityName != null)
{
// Replace database qualifier with alias, if any.
String alias = entityName.getPart(EntityName.DATABASE);
if (alias != null)
{
parts.set(0, alias);
}
// Replace table qualifier with alias, if any.
alias = entityName.getPart(EntityName.TABLE);
if (alias != null)
{
parts.set(1, alias);
}
}
// Compose a dot-delimited, fully qualified name from the part names in
// the list. If a part is still null at this point, omit it.
StringBuilder buf = new StringBuilder();
boolean first = true;
Iterator<String> iter = parts.iterator();
while (iter.hasNext())
{
String part = iter.next();
if (part == null)
{
continue;
}
if (!first)
{
buf.append('.');
}
first = false;
buf.append(part);
}
return buf.toString();
}
/**
* Must be called only for fields. Gets the field's ORDER setting.
*
* @return See above.
*/
long getOrder()
{
// TODO: can this even be used? the annotation is not available until fixups
if (ast.isAnnotation("order"))
{
return (Long) ast.getAnnotation("order");
}
// this path is used during parsing before fixups have converted child nodes into annotations
if (ast.downPath(ProgressParserTokenTypes.ORDER))
{
Aast oAst = ast.getImmediateChild(ProgressParserTokenTypes.ORDER, null);
return Long.parseLong(oAst.getFirstChild().getText());
}
// some schemata, when manually edited, can be missing the ORDER property; calculate an artificial
// ORDER based on index position
return (long) (ast.getIndexPos() + 1) * 10;
}
/**
* Comparator which requires the name portion of the {@code NameNode}s being compared lexically to match
* exactly for the nodes to be reported as equivalent (return value of {@code 0} from {@link
* Comparable#compareTo(java.lang.Object) compare} method). This is used when looking up an unabbreviated
* name, or when determining the natural order of name nodes within a {@link Namespace}.
*/
private static class ExactMatchComparator
implements Comparator<NameNode>,
Serializable
{
/**
* Resolves a comparison of two <code>NameNode</code> objects. After
* performing object reference identity check, and null checks, uses an
* exact lexical match of the nodes' names to determin equivalence.
*
* @param n1
* First <code>NameNode</code> for comparison. If
* <code>null</code>, <code>-1</code> is returned.
* @param n2
* Second <code>NameNode</code> for comparison. If
* <code>null</code>, <code>1</code> is returned.
*
* @return <code>0</code> if objects are considered equal by the rules
* of this comparator; <code>-1</code> if <code>n1</code> is
* considered less than <code>n2</code>; <code>1</code> if
* <code>n1</code> is considered greater than <code>n2</code>.
*/
public int compare(NameNode n1, NameNode n2)
{
// Both references refer to same object in memory.
if (n1 == n2)
{
return 0;
}
// Handle case where n1 is null.
if (n1 == null)
{
return -1;
}
// Handle case where n2 is null.
if (n2 == null)
{
return 1;
}
return n1.getName().compareTo(n2.getName());
}
}
/**
* Comparator which will report two <code>NameNode</code> objects as
* equivalent if the second node's name represents an abbreviation for the
* first node's name. This is used when looking up a name which may be
* abbreviated, when an exact match lookup has already failed.
* <p>
* <strong>Note:</strong> this comparator imposes orderings that are
* inconsistent with equals.
*/
private static class AbbreviationComparator
extends ExactMatchComparator
{
/**
* Resolves a comparison of two <code>NameNode</code> objects. The
* criteria for this comparison to determine equivalence is either an
* exact lexical match of the nodes' names, or that the name of node
* <code>n1</code> lexically begins with the name of node
* <code>n2</code>.
* <p>
* <strong>Important Node:</strong> this implementation supports the
* lookup of an abbreviated name within a sorted list of unabbreviated
* names. It relies upon the binary search implementation provided by
* <code>java.util.Collections</code>, in that it assumes the search key
* will always be <code>n2</code>, and <code>n1</code> always represents
* a node from the list being searched. <strong>This assumption is
* fundamental to the correct behavior of the {@link Namespace#find}
* method.</strong>
*
* @param n1
* First <code>NameNode</code> for comparison.
* @param n2
* Second <code>NameNode</code> for comparison.
*
* @return <code>0</code> if nodes' names are considered equal
* according to the rules described above; <code>-1</code> if
* <code>o1</code> is considered lexically less than
* <code>o2</code>; <code>1</code> if <code>o1</code> is
* considered lexically greater than <code>o2</code>.
*/
public int compare(NameNode n1, NameNode n2)
{
// Let superclass implementation try to detect an exact match first.
int result = super.compare(n1, n2);
// If superclass would sort n2 ahead of n1, check whether n1's name
// begins with n2's name (i.e., n2 is an abbreviation for n1).
if (result > 0 && n1.getName().startsWith(n2.getName()))
{
result = 0;
}
return result;
}
}
}