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