CallGraphWorker.java

/*
** Module   : CallGraphWorker.java
** Abstract : provides a pattern engine service to create a new call graph based on matches
**            found during AST processing
**
** Copyright (c) 2005-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 GES 20050311   @20310 Created initial version that provides support for creating the call
**                           graph in memory during pattern engine processing. It provides a
**                           dynamic iterator that walks the call graph and returns File objects
**                           for each node until the entire graph is complete. This iterator
**                           changes its return value dynamically based on inline changes to the
**                           graph. This is the key to driving the pattern engine from a single
**                           root entry point and being able to create an entire graph of any
**                           number of ASTs since the next AST for the pattern engine is read
**                           from the call graph that was just created by pattern engine
**                           processing.
** 002 GES 20050314   @20321 Added support for RUN VALUE() based call graph nodes using hints.
** 003 GES 20050316   @20397 Converted this worker to use the visitAst() interface in
**                           PatternWorker instead of using a rule driven approach.  
** 004 ECF 20050526   @21290 Changes to support new expression engine implementation. Changed
**                           library registration mechanism based on superclass' modifications
**                           to support single library per pattern worker limit.
** 005 SIY 20050531   @21341 Minimal changes to enable CallGraphWorker use .ast files instead 
**                           of .p.
** 006 SIY 20050606   @21453 Removed redundant methods for the call graph processing. Added
**                           methods for the "dead" files processing. Minor cleanups.  
** 007 SIY 20050622   @21544 Added absolute application path handling.
** 008 SIY 20051011   @23018 Fixed null pointer exception during loading of the hints file. 
** 009 GES 20060129   @24131 Minor name change for FileList.
** 010 GES 20060130   @24140 Safety code added to ensure that this worker could be used > 1 time
**                           per JVM process.
** 011 GES 20060130   @24157 Honor debug settings before writing output to the terminal.
** 012 SIY 20060312   @24981 Improved include files path handling to avoid incorrect marking them
**                           as not referenced.
** 013 GES 20090429   @42061 Match package and class name changes.
** 014 GES 20090514   @42233 Removed dead code.
** 015 CA  20140313          Moved to a graph DB implementation.  Added support for various 
**                           call-sites, mos
** 016 CA  20140408          Fixed filename matching for source-code originating from a different
**                           OS than the one where conversion is ran.
** 017 CA  20140424          Fixed disambiguation of call sites for which the hints provide no 
**                           targets.
** 018 ECF 20160407          Added TODO in loadIncludeFiles to check hints for an include-spec
**                           setting.
** 019 OM  20170123          Removed dead code.
** 020 GES 20170424          Changes for call graph analysis v3.
** 021 CA  20180327          Added search index support for reverse-filename.
**                           Added function-name and libname indexes.
**                           Other performance optimizations.
**                           Reworked hints to use values like ext-prog.p:proc-name, instead of 
**                           hint suffix.
** 022 CA  20180412          More indexes to improve performance.
**     CA  20180413          nextExternalId is expensive, cache the current value in the graph
**                           and use that to compute next IDs.
**                           Cache the vertex parents in a parent-node-type-<node-type> property 
**                           to avoid expensive computation.
**     CA  20180414          Cache the non-virtual function/procedure vertices, as the application 
**                           may have millions of these (including virtual), and executing millions
**                           of queries for each call-site or virtual entry point will be slow.
**     CA  20180418          Mark all reachable entry points, to improve the dead code report.
**                           More indexes and other performance optimizations.
**     CA  20180507          Compute line/col from first descendant with line/col different than 0.
**                           In update mode, re-process only files which have been processed.
**                           Added path aliasing (r-code to source) support.
**     CA  20180509          Allow include-level hints to disambiguate call sites.
**     CA  20180525          Callgraph improvements - changes in tx management, vertex caching 
**                           and more.
** 023 CA  20181003          Added indexes for OO blocks and entry-point calculation.
** 024 CA  20181123          Basic support for multiple classes with the same qualified names
**     CA  20181128          Fixed ambiguous callsite processing (getCallSiteHintTargets must
**                           return null if no hints are defined).
** 025 CA  20200428          Some changes to process DEFINE ENUM.  Untested.
** 026 CA  20211214          The AST manager plugin must be context-local, for runtime conversion, as the 
**                           InMemoryRegistryPlugin is not thread-safe.
** 027 TJD 20240208          Java 17 dependencies updates
**     TJD 20240216          Newer versions of JanusDB require MixedTypeIndexes when using aggregates
*/

/*
** 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.uast;

import java.io.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;

import com.goldencode.ast.*;
import com.goldencode.graphdb.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.pattern.*;
import com.goldencode.p2j.preproc.*;
import com.goldencode.p2j.util.*;

import org.janusgraph.graphdb.types.system.SystemTypeManager;
import org.apache.tinkerpop.gremlin.structure.*;
import org.apache.tinkerpop.gremlin.process.traversal.*;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.*;
import static org.janusgraph.core.attribute.Text.*;

/**
 * Provides a pattern engine service to create a new call graph based on matches found during AST
 * processing.  Only one call graph can be created at a time due to use of static members.
 * <p>
 * This call graph contains external file references, schema tables and external link sites (as
 * native processes or web services) AND all duplications have been removed.
 * <p>
 * This worker provides generic routines, so other call-sites can be added in the future.
 * <p>
 * Ambiguous call-sites can be disambiguated using UAST hints to specify 1 or more targets (see
 * {@link UastHints}); note that such hints are named using the call-site-key as prefix,
 * then an instance number specifying the 0-based index of this match in the given source file.
 */
public class CallGraphWorker
extends AbstractPatternWorker
implements ProgressParserTokenTypes
{
   /**
    * List of targets to be processed. This starts with the initial entry-points and acts
    * as a queue to which called external programs are added.
    */
   private static final LinkedList<Vertex> targets = new LinkedList<>();

   /** The connected graph DB. */
   private static Graph db = null;

   /** Stores the current AST's hints object. */
   private UastHints hints = null;
   
   /** Caches the include-level hint files. */
   private Map<String, UastHints> includeHints = new HashMap<>();

   /** For each include file, keep a counter of hint IDs, for each hint type. */
   private Map<String, Map<Integer, AtomicInteger>> includeHintsNextIds = new HashMap<>();
   
   /** Stores the current AST's preprocessor hints object. */
   private PreprocessorHints pphints = null;

   /** <code>true</code> if no output should be written to the terminal. */
   private boolean silent = false;
   
   /** Flag indicating if we are starting to collect targets. */
   private boolean collectTargets = false;
   
   /** Map of aliases (r-code folders) to their source folders (conversion folders). */
   private Map<String, String> pathAliases = null;
   
   /**
    * Default constructor which calls the super-class constructor, registers
    * its libraries and initializes its instance members.
    * 
    * @throws  AstException
    *          Forwarded from underlying methods.
    */
   public CallGraphWorker()
   throws AstException
   {
      super();
      setLibrary(new CallGraph());
      
      // honor debug settings
      silent = (PatternEngine.getDebugLevel() == PatternEngine.MSG_NONE);
      
      // initialize the graph DB
      Graph db = getGraph();
      if (db.tx().isOpen())
      {
         db.tx().commit();
         db.tx().close();
      }
      
      db.tx().open();
      
      // load the path aliases
      pathAliases = Configuration.getPathAliases();
   }
   
   /**
    * Initialize the {@link #db graph DB} as necessary and return it.
    * <p>
    * The graph DB will connect to the folder specified via the {@code callgraph-db-folder} 
    * parameter and it will default to {@link CallGraphHelper#DEFAULT_CALLGRAPH_DB_FOLDER},
    * if not set.
    * <p>
    * The graph DB is configured for a JanusGraph implementation, backed by {@code persistit}
    * storage and with {@code lucene} indexes.
    * 
    * @return   The graph DB instance associated with the call-graph.
    */
   public static Graph getGraph()
   {
      if (db == null)
      {
         db = CallGraphHelper.getGraph();
         
         // unique indexes
         GraphDB.createIndex(db, "os-filename",       String.class,  true,  true);
         GraphDB.createIndex(db, "filename",          String.class,  true,  true);
         GraphDB.createIndex(db, "reverse-filename",  String.class,  true,  false, true);
         GraphDB.createIndex(db, "reverse-compiled-filename", String.class, true, false, true);
         GraphDB.createIndex(db, "port-type",         String.class,  true,  true);
         GraphDB.createIndex(db, "schemaname",        String.class,  true,  true);
         GraphDB.createIndex(db, "command",           String.class,  true,  true);
         GraphDB.createIndex(db, "connect-string",    String.class,  true,  true);
         GraphDB.createIndex(db, "com-target",        String.class,  true,  true);
         GraphDB.createIndex(db, "dde-target",        String.class,  true,  true);
         GraphDB.createIndex(db, "event-name",        String.class,  true,  false);
         GraphDB.createIndex(db, "procedure",         String.class,  true,  false);
         GraphDB.createIndex(db, "function",          String.class,  true,  false);
         GraphDB.createIndex(db, "missing_function",  String.class,  true,  true);
         GraphDB.createIndex(db, "missing_procedure", String.class,  true,  true);
         GraphDB.createIndex(db, "class-name",        String.class,  true,  false);
         GraphDB.createIndex(db, "method-name",       String.class,  true,  false);
         GraphDB.createIndex(db, "property-name",     String.class,  true,  false);
         GraphDB.createIndex(db, "node-key",          String.class,  true,  false);
         GraphDB.createIndex(db, "function-name",     String.class,  true,  false);
         GraphDB.createIndex(db, "libname",           String.class,  true,  false);
          
         GraphDB.createIndex(db, "node-type",        Integer.class, true,  false, true);
         GraphDB.createIndex(db, "node-id",          Long.class,    true,  false, true);
         GraphDB.createIndex(db, "call-site-key",    Integer.class, false, false);
         GraphDB.createIndex(db, "call-site-id",     Long.class,    false, false);
         GraphDB.createIndex(db, "trigger-type",     Integer.class, false, false);
         GraphDB.createIndex(db, "live",             Boolean.class, true,  false);
         GraphDB.createIndex(db, "reachable",        Boolean.class, true,  false);
         GraphDB.createIndex(db, "external",         Boolean.class, true,  false);
         GraphDB.createIndex(db, "included",         Boolean.class, true,  false);
         GraphDB.createIndex(db, "resource-domain",  Integer.class, true,  false);

         GraphDB.createIndex(db,
                             new String[] { "node-id", "node-type"  },
                             new Class<?>[] { Long.class, Integer.class },
                             true,  true);
         GraphDB.createIndex(db,
                             new String[] { "node-id", "resource-domain" },
                             new Class<?>[] { Long.class, Integer.class },
                             true,  false);
//         GraphDB.createIndex(db,
//                             new String[] { "node-key", "external" },
//                             new Class<?>[] { String.class, Boolean.class },
//                             true,  false);
         GraphDB.createIndex(db,
                             new String[] {  "function", "node-type" },
                             new Class<?>[] { String.class, Integer.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] { "procedure", "node-type" },
                             new Class<?>[] { String.class, Integer.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] {  "function", "live" },
                             new Class<?>[] { String.class, Boolean.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] { "procedure", "live" },
                             new Class<?>[] { String.class, Boolean.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] { "method-name", "node-type" },
                             new Class<?>[] { String.class, Integer.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] { "node-type", "event-name" },
                             new Class<?>[] { Integer.class , String.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] { "resource-domain", "os-filename" },
                             new Class<?>[] { Integer.class, String.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] {  "node-type", "app-entry-point" },
                             new Class<?>[] { Integer.class, Boolean.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] {  "external", "missing" },
                             new Class<?>[] { Boolean.class, Boolean.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] {  "procedure", "virtual" },
                             new Class<?>[] { String.class, Boolean.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] {  "function", "virtual" },
                             new Class<?>[] { String.class, Boolean.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] {  "node-type", "resource-domain", "reachable" },
                             new Class<?>[] { Integer.class, Integer.class, Boolean.class },
                             true,  false);
         GraphDB.createIndex(db,
                             new String[] {  "external", "missing", "reachable" },
                             new Class<?>[] { Boolean.class, Boolean.class, Boolean.class },
                             true,  false);
         
         String[] extKeys =  
         {
            "filename",
            "port-type",
            "command",
            "connect-string",
            "com-target",
            "dde-target",
            "procedure",
            "function",
            "missing_function",
            "missing_procedure",
            "class-name",
            "method-name",
            "property-name",
            "function-name",
            "libname",
         };
         
         for (String key : extKeys)
         {
            GraphDB.createIndex(db,
                                new String[] {  key, "node-key", "external" },
                                new Class<?>[] { String.class, String.class, Boolean.class },
                                true,  false);
         }
         
         int[] parents =
         {
            PROCEDURE_FILE,
            SCHEMA_FILE,
            FUNCTION,
            INTERNAL_PROCEDURE,
            EXTERNAL_PROCEDURE,
            TABLE,
            DATABASE,
            TRIGGER_BLOCK,
            METHOD_DEF,
            DEFINE_PROPERTY_SET,
            DEFINE_PROPERTY_GET,
            CONSTRUCTOR,
            DESTRUCTOR,
            CLASS_DEF,
            INTERFACE_DEF,
            ENUM_DEF
         };
         
         for (int p : parents)
         {
            String prop = CallGraphHelper.parentProperty(p);
          
            GraphDB.createIndex(db, prop, Long.class, true, false);

            GraphDB.createIndex(db,
                                new String[] {  prop, "entry-point" },
                                new Class<?>[] { String.class, Boolean.class },
                                true,  false);
         }
         
         String prop = CallGraphHelper.parentProperty(PROCEDURE_FILE);
         GraphDB.createIndex(db,
                             new String[] {  "node-type", prop },
                             new Class<?>[] { Integer.class , Long.class },
                             true,  false);
      }
      
      return db;
   }
   
   /**
    * Creates a new iterator that uses as initial nodes the explicit list of programs passed at
    * rootNodes parameter and also all the external programs with ambiguous call-sites.
    * <p>
    * The explicit targets in the rootNodes array will not be marked as application entry-points.
    * <p>
    * The call graph is walked dynamically, using the nodes as the tree is being built to properly
    * direct the pattern engine in its processing, and avoiding already-processed nodes.
    *
    * @param    rootNodes
    *           List of root AST names to use as entry points.
    * @param    relative
    *           Specifies if the name is relative to the project home directory.  If 
    *           <code>false</code>, the name is either an absolute filename or it is relative to
    *           the current directory.
    * 
    * @return   <code>Iterator</code> instance suitable for the node list traversal.
   */
   static Iterator<Vertex> newUpdateCallGraphIterator(String[] rootNodes, boolean relative)
   {
      long aid = CallGraphHelper.AMBIGUOUS_NODE_ID;
      
      targets.clear();
      
      loadRootNodes(rootNodes, relative, false);
      
      // ensure the AST Manager is initialized
      new PatternEngine();
      AstManager mgr = AstManager.get();
      
      // load the ambiguous nodes
      Vertex ambiguous = CallGraphHelper.findNodeById(AMBIGUOUS, aid);
      
      Iterator<Vertex> itr = db.traversal().V(ambiguous).in().has("reachable", true);
      
      Set<String> ambigExtProgs = new HashSet<>();
      
      while (itr.hasNext())
      {
         Vertex node = itr.next();
         
         String filename = mgr.getTreeName((long) node.<Long>property("node-id").value());
         if (filename != null)
         {
            filename = CallGraphHelper.prepareFilename(filename);
            Vertex procFile = CallGraphHelper.findUniqueNode("filename", filename);
            
            if (!ambigExtProgs.contains(filename) && procFile.property("finished").isPresent())
            {
               ambigExtProgs.add(filename);

               procFile.<Boolean>property("processing", true);
               targets.add(procFile);
            }
         }
      }
      
      return targetsIterator();
   }
   
   /**
    * Creates a new iterator that walks a call graph dynamically, using the nodes as the tree is
    * being built to properly direct the pattern engine in its processing.  Advancing to the next
    * node is done when the tree is changed rather than when the next() method is called.
    * <p>
    * The explicit targets in the rootNodes array will be marked as application entry-points.
    *
    * @param    rootNodes
    *           List of root AST names to use as entry points.
    * @param    relative
    *           Specifies if the name is relative to the project home directory.  If 
    *           <code>false</code>, the name is either an absolute filename or it is relative to
    *           the current directory.
    * 
    * @return   <code>Iterator</code> instance suitable for the node list traversal.
   */
   static Iterator<Vertex> newCallGraphIterator(String[] rootNodes, boolean relative)
   {
      // clear the targets and load the root nodes
      targets.clear();

      loadRootNodes(rootNodes, relative, true);

      return targetsIterator();
   }
   
   /**
    * Create a new iterator over the {@link #targets} queue, by polling elements from the
    * head of the queue until the queue is empty.
    * 
    * @return   See above.
    */
   private static Iterator<Vertex> targetsIterator()
   {
      return new Iterator<Vertex>()
      {
         @Override
         public void remove()
         {
            throw new UnsupportedOperationException();
         }

         @Override
         public Vertex next()
         {
            return targets.pollFirst();
         }

         @Override
         public boolean hasNext()
         {
            return !targets.isEmpty();
         }
      };
   }
   
   /**
    * Load the specified root nodes into the {@link #targets}.
    * 
    * @param    rootNodes
    *           List of root AST names to use as entry points.
    * @param    relative
    *           Specifies if the name is relative to the project home directory.  If 
    *           <code>false</code>, the name is either an absolute filename or it is relative to
    *           the current directory.
    * @param    entryPoint
    *           Flag to mark or not the nodes as application entry-points.
    */
   private static void loadRootNodes(String[] rootNodes, 
                                     boolean  relative, 
                                     boolean  entryPoint)
   {
      if (rootNodes == null)
      {
         return;
      }
      
      List<Vertex> nodes = CallGraphHelper.loadFileNodes(rootNodes, relative);
      
      for (Vertex v : nodes)
      {
         if (entryPoint)
         {
            v.<Boolean>property("app-entry-point", true);
         }

         v.<Boolean>property("processing", true);

         targets.add(v);
      }
   }
   
   /**
    * Reverse the given string.
    * 
    * @param    s
    *           The string to reverse.
    *           
    * @return   The reversed string.
    */
   private static String reverseString(String s)
   {
      char[] chars = s.toCharArray();
      for (int i = 0; i < chars.length / 2; i++)
      {
         // switch chars in-place
         int idx2 = chars.length - (i + 1);
         char aux = chars[i];
         chars[i] = chars[idx2];
         chars[idx2] = aux;
      }
      
      return new String(chars);
   }

   /**
    * Resets the current hints object and the counters for all call-sites and each time a new AST
    * is visited.  This enables the lazy loading of hints files only when needed and provides the
    * ability to calculate the hint name corresponding to a specific call-site in a given file.
    *
    * @param   ast
    *          The root node of the source AST about to be processed by the pattern engine.
    */
   public void visitAst(Aast ast)
   {
      hints = null;
      pphints = null;
      includeHintsNextIds.clear();
   }

   /**
    * A container for the hints referring to a call-site disambiguation.
    */
   public class CallSiteHint
   {
      /** The hint's suffix. */
      private final String suffix;
      
      /** The call-site instance (numbered in order of appearance). */
      private final int instance;
      
      /** The index in the hints's array, otherwise -1. */
      private final int index;
      
      /** An additional info. */
      private final int additional;
      
      /** Flag indicating this is from an include. */
      private boolean include;
      
      /**
       * Create a new call-site hint.
       * 
       * @param    instance
       *           The call-site instance (numbered in order of appearance).
       * @param    index
       *           The index in the hints's array, otherwise -1.
       * @param    additional
       *           An additional info.
       * @param    suffix
       *           The hint's suffix.
       * @param    include
       *           Flag indicating this hint is from an include file.
       */
      public CallSiteHint(int instance, int index, int additional, String suffix, boolean include)
      {
         this.instance = instance;
         this.index = index;
         this.additional = additional;
         this.suffix = suffix;
         this.include = include;
      }
      
      /**
       * Determine if this hint is from an include file or not.
       * 
       * @return   See above.
       */
      public boolean isInclude()
      {
         return include;
      }
      
      /**
       * Get the hint's suffix.
       * 
       * @return   The {@link #suffix}.
       */
      public String getSuffix()
      {
         return suffix;
      }
      
      /**
       * Create a string representation of this instance.
       * 
       * @return   See above.
       */
      @Override
      public String toString()
      {
         StringBuilder sb = new StringBuilder();
         sb.append(instance);
         if (index >= 0)
         {
            sb.append("_").append(index);
         }
         if (additional >= 0)
         {
            sb.append(" ").append(additional);
         }
         
         if (suffix != null && !suffix.isEmpty())
         {
            sb.append(" ").append(suffix);
         }
         
         return sb.toString();
      }
      
      /**
       * Compute the hashcode of this instance.
       * 
       * @return   See above.
       */
      @Override
      public int hashCode()
      {
         int result = 17;
         result = result * 37 + instance;
         result = result * 37 + index;
         result = result * 37 + additional;
         result = result * 37 + (suffix == null ? 0 : suffix.hashCode());
         
         return result;
      }
      
      /**
       * Check if this instance is the same as the given one.
       * 
       * @param    obj
       *           The instance to compare against.
       * 
       * @return   See above.
       */
      @Override
      public boolean equals(Object obj)
      {
         if (!(obj instanceof CallSiteHint))
         {
            return false;
         }

         CallSiteHint csh = (CallSiteHint) obj;
         
         return csh.instance == instance     &&
                csh.index == index           &&
                csh.additional == additional &&
                ((csh.suffix == null && suffix == null) || csh.suffix.equals(suffix));
      }
   }
   
   /**
    * Provides a service for creating call graph nodes in a tree starting at some root node.
    * This class is a <code>PatternEngine</code> worker.
    */
   public class CallGraph
   {
      /** The number of created edges since last opened transaction. */
      private long txNewEdges = 0;
      
      /** The number of created vertices since last opened transaction. */
      private long txNewVertices = 0;
      
      /** The number of set properties since last opened transaction. */
      private long txChangedProperties = 0;
      
      /** A cache of computed node IDs for external targets. */
      private Map<Integer, AtomicLong> externalIds = new HashMap<>();
      
      /**
       * Map of non-virtual functions (by name with vertex ID list).  There are 3 indices:
       * <ul>
       *    <li>index 0 - only vertex IDs part of ADM programs.</li>
       *    <li>index 1 - only vertex IDs NOT part of ADM programs.</li>
       *    <li>index 2 - all vertex IDs from both 0 and 1 indexes.</li>
       * </ul>
       */
      private Map<String, List<Object>[]> functions = null;
      
      /**
       * Map of non-virtual procedures (by name with vertex ID list).  There are 3 indices:
       * <ul>
       *    <li>index 0 - only vertex IDs part of ADM programs.</li>
       *    <li>index 1 - only vertex IDs NOT part of ADM programs.</li>
       *    <li>index 2 - all vertex IDs from both 0 and 1 indexes.</li>
       * </ul>
       */
      private Map<String, List<Object>[]> procedures = null;
      
      /** The ambiguous vertex. */
      private Vertex ambiguousVertex = null;
      
      /**
       * Set the new state of the {@link CallGraphWorker#collectTargets} flag.
       * 
       * @param    state
       *           {@code true} to start collecting new targets, {@code false} otherwise.
       */
      public void setCollectTargets(boolean state)
      {
         collectTargets = state;
      }
      
      /**
       * Get the include source file for this AST.  If the source is not from an include file,
       * return {@code null}.
       * 
       * @param    ast
       *           The AST for which the include source is needed.
       * @param    line
       *           The line in the source code.
       * @param    column
       *           The column in the source code.
       * 
       * @return   See above.
       * 
       * @throws   PreprocessorException
       *           An unknown or unexpected failure.
       */
      public String getIncludeSourceFor(Aast ast, int line, int column)
      throws PreprocessorException
      {
         if (ast.isRoot())
         {
            return null;
         }
         
         if (line == 0 || column == 0)
         {
            final String msg = "WARNING: can not determine the include file for synthetic ASTs, " +
                               "with no line/column information: %d";
            println(String.format(msg, ast.getId()));
            return null;
         }
         
         PreprocessorHints hints = loadPreprocessorHints(ast);
         
         String includeSource = hints.getSourceFor(line, column);
         includeSource = CallGraphHelper.prepareFilename(includeSource);
         String filename = CallGraphHelper.prepareFilename(ast.getFilename());

         return (includeSource.equals(filename)) ? null : includeSource;
      }
      
      /**
       * Prepare the procedure file name by replacing all windows-style separators with linux-style 
       * separators and making the name lowercase, if we are on a case-insensitive OS.
       * 
       * @param   filename
       *          Source file name.
       * 
       * @return  Prepared file name.
       */
      public String prepareFilename(String filename)
      {
         return CallGraphHelper.prepareFilename(filename);
      }
      
      /**
       * Mark this vertex and all its "contains" children as being live - this means the vertex
       * was reached during call graph processing, and all its internal entries are available
       * for targeting.
       * 
       * @param    v
       *           The procedure vertex to mark.
       */
      public void markLive(Vertex v)
      {
         if (v.property("live").isPresent())
         {
            return;
         }
         
         property(v, "live", true);

         traversalIterator(v, "contains", true).forEachRemaining(vp -> markReachable(vp));
      }

      /**
       * Mark all reachable vertices from this node as 'reachable'.
       * 
       * @param    v
       *           The node from which to start reachable marking.
       */
      public void markReachable(Vertex v)
      {
         if (v.property("reachable").isPresent())
         {
            return;
         }
         
         property(v, "reachable", true);

         traversalIterator(v, "contains", false).forEachRemaining(vp -> markReachable(vp));
         
         db.traversal().V(v)
           .outE("references", "inherits", "implements", "override", "listens", "includes")
           .inV()
           .hasNot("reachable")
           .forEachRemaining(ref -> markReachable(ref));
      }
      
      /**
       * Check if we are in a case-sensitive OS, by reading the {@code case-sensitive} configuration
       * parameter.
       * 
       * @return   {@code true} if the OS filenames are case-sensitive.
       */
      public boolean isCaseSensitive()
      {
         return CallGraphHelper.isCaseSensitive();
      }

      /**
       * Get the graph node to which all ambiguous call-sites link. This is temporary until the
       * call-site ASTs are added into the callgraph.
       * 
       * @return   See above.
       */
      public Vertex ambiguousNode()
      {
         if (ambiguousVertex != null)
         {
            return ambiguousVertex;
         }
         
         long aid = CallGraphHelper.AMBIGUOUS_NODE_ID;
         
         Vertex node = CallGraphHelper.findNodeById(AMBIGUOUS, aid);
         if (node == null)
         {
            node = createNode("ambiguous", aid, AMBIGUOUS, UNKNOWN_RESOURCE);
         }
         
         ambiguousVertex = node;
         
         return node;
      }
      
      /**
       * For the specified node, remove all the edges in the given direction which match the
       * label, property key and property value.
       * 
       * @param    node
       *           A graph node.
       * @param    dir
       *           {@code true} for out edges and {@code false} for in edges.
       * @param    label
       *           The edge's label.
       * @param    key
       *           The property's key.
       * @param    value
       *           The property's value.
       */
      public void removeEdges(Vertex node, boolean dir, String label, String key, Object value)
      {
         GraphTraversal<?, Vertex> gv   = db.traversal().V(node);
         GraphTraversal<?, Edge>   ge   = dir ? gv.outE() : gv.inE();
         ge.hasLabel(label).has(key, value).forEachRemaining(edge -> edge.remove());
      }
      
      /**
       * For the specified node, remove all the edges in the given direction which match the 
       * property key and property value.
       * 
       * @param    node
       *           A graph node.
       * @param    dir
       *           {@code true} for out edges and {@code false} for in edges.
       * @param    key
       *           The property's key.
       * @param    value
       *           The property's value.
       */
      public void removeEdges(Vertex node, boolean dir, String key, Object value)
      {
         GraphTraversal<?, Vertex> gv   = db.traversal().V(node);
         GraphTraversal<?, Edge>   ge   = dir ? gv.outE() : gv.inE();
         ge.has(key, value).forEachRemaining(edge -> edge.remove());
      }
      
      /**
       * Create a new graph DB node associated with the given AST, representing a permanent schema
       * table.
       * 
       * @param    schema
       *           The root database schema vertex.
       * @param    ast
       *           The schema table AST.
       * @param    schemaname
       *           The table's full name.
       *           
       * @return   The created {@link Vertex node}.
       * 
       * @throws   PreprocessorException
       *           An unknown or unexpected failure.
       */
      public Vertex addTable(Vertex schema, Aast ast, String schemaname)
      throws PreprocessorException
      {
         Vertex node = createAstNode(schema, ast, TABLE, true);
         
         node.<String>property("schemaname", schemaname);
         node.<String>property("node-key", "schemaname");
         
         return node;
      }
      
      /**
       * Remove the specified property from the given vertex or edge.
       * 
       * @param    element
       *           The vertex or edge from which to remove the property.
       * @param    key
       *           The property name.
       */
      public void removeProperty(Element element, String key)
      {
         Property<?> vp = element.property(key);
         if (vp.isPresent())
         {
            vp.remove();
            
            ++txChangedProperties;
            commit(false);
         }
      }
      
      /**
       * Get the specified property from the given vertex or edge.
       * 
       * @param    element
       *           The vertex or edge on which to set the property.
       * @param    key
       *           The property name.
       *           
       * @return   The property's value or <code>null</code> if is not present at the node.
       */
      public Object property(Element element, String key)
      {
         return CallGraphHelper.property(element, key);
      }
      
      /**
       * Set the given property on the given vertex or edge.  This is a helper function to handle 
       * the lack of generics support in TRPL.
       *
       * @param    element
       *           The vertex or edge on which to set the property.
       * @param    key
       *           The property name.
       * @param    value
       *           The value to set.
       */
      public void property(Element element, String key, Object value)
      {
         CallGraphHelper.property(element, key, value);
         
         ++txChangedProperties;
         commit(false);
      }
      
      /**
       * Create a new graph DB node associated with the given file resource.
       *
       * @param    label
       *           The label for the new vertex.
       * @param    type
       *           The "node-type" property for the new vertex.
       * @param    filename
       *           The OS-level filename of the resource.
       *           
       * @return   The created {@link Vertex node}.
       */
      public Vertex createFileResource(String label, int type, String filename)
      {
         long   id    = nextExternalId(type);
         Vertex fnode = createNode(label, id, type, FILE_RESOURCE);
         
         // need to use linux-style separators
         String fullName = CallGraphHelper.prepareFilename(filename);
         
         fnode.<String>property("os-filename", filename);
         fnode.<String>property("filename", fullName);
         fnode.<String>property("reverse-filename", reverseString(fullName));
         fnode.<String>property("node-key", "filename");
         
         if (type == PROCEDURE_FILE)
         {
            String compiledName = fullName;
            if (!compiledName.toLowerCase().endsWith(".r"))
            {
               int lastDot = compiledName.lastIndexOf(".");
               compiledName = compiledName.substring(0, lastDot) + ".r";
            }
            fnode.<String>property("reverse-compiled-filename", reverseString(compiledName));
         }

         return fnode;
      }
      
      /**
       * Find or create the vertex associated with the specified event.
       * 
       * @param    container
       *           The container where the event is being listened.
       * @param    event
       *           The event name, kept at the <code>event-name</code> vertex property.
       *           
       * @return   See above.
       */
      public Vertex findOrCreateEvent(Vertex container, String event)
      {
         if (event.length() == 0)
         {
            throw new IllegalStateException("Could not create an event with empty name!");
         }
         
         if (event.length() > 1)
         {
            event = event.toUpperCase();
         }
         
         Iterator<Vertex> iter = db.traversal()
                                   .V(container)
                                   .outE("contains")
                                   .inV()
                                   .has("node-type", EVENT)
                                   .has("event-name", event)
                                   .limit(1);
         if (iter.hasNext())
         {
            return iter.next();
         }

         long   id    = nextExternalId(EVENT);
         Vertex fnode = createNode(event, id, EVENT, EVENT);
         
         fnode.<String>property("node-key", "event-name");
         fnode.<String>property("event-name", event);
         
         createEdge(container, fnode, "contains");
         
         return fnode;
      }
      
      /**
       * Create a new graph DB node associated with the given AST which is by some external
       * resource. There will be a "contains" edge between the new node (in) and the
       * containing node (out).  The state of the AST will be available via properties.
       * 
       * @param    cntr
       *           The external resource (often a procedure file) that contains this node.
       * @param    ast
       *           The AST representing the entry point.
       * @param    type
       *           The "node-type" property for the new vertex.
       * @param    entry
       *           <code>true</code> if this is an entry-point, <code>false</code> for a
       *           call-site. This will be stored in a property called "entry-point".
       *           
       * @return   The created {@link Vertex node}.
       * 
       * @throws   PreprocessorException
       *           An unknown or unexpected failure.
       */
      public Vertex createAstNode(Vertex cntr, Aast ast, int type, boolean entry)
      throws PreprocessorException
      {
         // create the AST vertex
         Vertex node = createNode(ast.getSymbolicTokenType(),
                                  ast.getId(),
                                  type,
                                  AST_NODE);
         
         // create the linkage to the procedure file in which this exists 
         Edge edge = createEdge(cntr, node, "contains");

         // remember if it is a call-site or entry-point
         node.<Boolean>property("entry-point", entry);
         
         Aast ref = ast;
         int line = ref.getLine();
         int column = ref.getColumn();
         
         while ((line == 0 || column == 0) && ref.getFirstChild() != null)
         {
            ref = (Aast) ref.getFirstChild();
            line = ref.getLine();
            column = ref.getColumn();
         }

         // save core AST state values as properties
         node.<Integer>property("column", column);
         node.<Integer>property("line",   line);
         node.<String>property("text",   ast.getText());
         node.<Integer>property("type",   ast.getType());
         node.<String>property("node-key", "ast-type");
         node.<String>property("ast-type", ProgressParser.lookupTokenName(ast.getType()));

         // load all annotations from this AST
         Iterator<String> itr = ast.annotationKeys();
         while (itr.hasNext())
         {
            String key = itr.next();
            Object val = ast.getAnnotation(key);
      
            if (node.property(key).isPresent() || SystemTypeManager.isSystemType(key))
            {
               if (!"type".equals(key))
               {
                  println(String.format("WARNING: renaming key [%s] to [annotation_%s]", key, key));
               }
               key = "annotation_" + key;
            }
            
            // graph doesn't support list properties
            if (!(val instanceof List))
            {
               node.<Object>property(key, val);
            }
         }

         String includeSource = getIncludeSourceFor(ast, line, column);
         boolean included = includeSource != null;
         property(node, "included", included);
         
         cacheParents(ast, node);

         if (included)
         {
            Vertex include = CallGraphHelper.findUniqueNode("filename", includeSource);
            // the direction must be from the 4GL code to the include file
            createEdge(node, include, "includes");
            
            // the original location in the include file can not be determined

            if (!entry)
            {
               setIncludeHints(node, type);
            }
         }
         
         return node;
      }
      
      /**
       * Find an edge between the two nodes, which has the specified label and a property with
       * the given key and value.
       * 
       * @param    out
       *           The source node.
       * @param    in
       *           The target node.
       * @param    label
       *           The edge's label.
       * @param    edgeKey
       *           The property's key.
       * @param    edgeValue
       *           The property's value.
       *           
       * @return   The first found edge or {@code null} if not found.
       */
      public Edge findEdge(Vertex out, Vertex in, String label, String edgeKey, Object edgeValue)
      {
         Iterator<Edge> iter = db.traversal()
                                 .V(out)
                                 .outE(label)
                                 .has(edgeKey, edgeValue)
                                 .as("e")
                                 .inV().hasId(in.id()).select("e");

         return iter.hasNext() ? iter.next() : null;
      }
      
      /**
       * Find an edge between the two nodes, which has the specified label.
       * 
       * @param    out
       *           The source node.
       * @param    in
       *           The target node.
       * @param    label
       *           The edge's label.
       *           
       * @return   The first found edge or {@code null} if not found.
       */
      public Edge findEdge(Vertex out, Vertex in, String label)
      {
         Iterator<Edge> iter = db.traversal()
                                 .V(out)
                                 .outE(label)
                                 .filter(e -> e.get().inVertex().equals(in));

         return iter.hasNext() ? iter.next() : null;
      }
      
      /**
       * Create an external node with the given type.
       * 
       * @param    label
       *           The label for the new vertex.
       * @param    type
       *           The "node-type" property for the new vertex.
       * @param    domain
       *           The token type of the node's resource domain, will be stored as the
       *           "resource-domain" property.
       *           
       * @return   The created {@link Vertex node}.
       */
      public Vertex createExternalNode(String label, int type, int domain)
      {
         Vertex node = createNode(label, nextExternalId(type), type, domain);
         
         node.<Boolean>property("external", true);
         
         return node;
      }
      
      /**
       * Find the external node with the specified property.
       * 
       * @param    nodeKey
       *           The property name.
       * @param    nodeValue
       *           The property value.
       *           
       * @return   The found node, but only if only one node was found.
       */
      public Vertex findExternalNode(String nodeKey, Object nodeValue)
      {
         Iterator<Vertex> iter = db.traversal().V().has(nodeKey, nodeValue)
                                                   .has("node-key", nodeKey)
                                                   .has("external", true)
                                                   .limit(2);
         
         Vertex v = null;
         
         if (iter.hasNext())
         {
            v = iter.next();
            
            if (iter.hasNext())
            {
               throw new IllegalStateException("More than one node found!");
            }
         }
         
         return v;
      }
      
      /**
       * Create a new {@link Vertex node} with the specified ID and type.
       *
       * @param    label
       *           The label for the new vertex.
       * @param    id
       *           The node's unique identifier, will be stored as the "node-id" property.
       * @param    type
       *           The token type for the node, will be stored as the "node-type" property.
       * @param    domain
       *           The token type of the node's resource domain, will be stored as the
       *           "resource-domain" property.
       *           
       * @return   The created {@link Vertex node}.
       */
      public Vertex createNode(String label, long id, int type, int domain)
      {
         if (label.trim().isEmpty())
         {
            // placeholder for SPACE - as Janus doesn't allow whitespace string 
            label = "SPACE";
         }
         
         Vertex node = db.addVertex(label);
         
         node.<Long>property("node-id", id);
         node.<Integer>property("node-type", type);
         node.<Integer>property("resource-domain", domain);
         
         ++txNewVertices;
         commit(false);
         
         return node;
      }
      
      /**
       * Create a new edge between the specified nodes, with the given label.
       * 
       * @param    out
       *           The source node.
       * @param    in
       *           The target node.
       * @param    label
       *           The edge label.
       *           
       * @return   The created {@link Edge}.
       */
      public Edge createEdge(Vertex out, Vertex in, String label)
      {
         Edge edge = out.addEdge(label, in);
         
         // this is useful only for the GraphML - as the edge's label is not serialized
         edge.<String>property("edge-type", label);
         
         ++txNewEdges;
         commit(false);
         
         return edge;
      }
      
      /**
       * Check if the two lists of comma-separated parameter data types match.
       * 
       * @param    expected
       *           The expected data types.
       * @param    actual
       *           The actual data types.
       * 
       * @return   <code>true</code> if the two match.
       */
      public boolean parameterTypeMatch(String expected, String actual)
      {
         if (expected.isEmpty() && actual.isEmpty())
         {
            return true;
         }

         if (expected.isEmpty() != actual.isEmpty())
         {
            return false;
         }
         
         String[] stl1 = expected.split(",");
         String[] stl2 = actual.split(",");
         
         if (stl1.length != stl2.length)
         {
            return false;
         }
         
         for (int i = 0; i < stl1.length; i++)
         {
            String t1 = stl1[i].toLowerCase();
            String t2 = stl2[i].toLowerCase();
            
            if (t1.equals("poly") || t2.equals("poly"))
            {
               continue;
            }
            
            if (!ExpressionConversionWorker.isAssignmentCompatible(t1, t2))
            {
               // check if these are classes... if so, the expected must be the same or a super
               // class of actual
               
               Iterator<Vertex> ecls = CallGraphHelper.findClassNode(t1);
               if (!ecls.hasNext())
               {
                  return false;
               }
               
               Iterator<Vertex> acls = CallGraphHelper.findClassNode(t2);
               if (!acls.hasNext())
               {
                  return false;
               }
               
               Set<String> supers = classHierarchy(t2);
               if (!supers.contains(t1))
               {
                  return false;
               }
            }
         }
         
         return true;
      }
      
      /**
       * Resolve the class hierarchy starting with the current class.
       * 
       * @param    clsName
       *           The class name.
       *           
       * @return   The inherits hierarchy from the given class.
       */
      public Set<String> classHierarchy(String clsName)
      {
         clsName = clsName.toLowerCase();

         Set<String> res = new LinkedHashSet<>();
         res.add(clsName);
         
         Iterator<Vertex> clsVIter = CallGraphHelper.findClassNode(clsName);

         if (!clsVIter.hasNext())
         {
            System.out.println(String.format("WARNING: Class %s is not loaded in the callgraph!", 
                                             clsName));

            return res;
         }
         
         while (clsVIter.hasNext())
         {
            Vertex clsV = clsVIter.next();

            Iterator<Vertex> superC = db.traversal().V(clsV).outE("inherits").inV();
            if (superC.hasNext())
            {
               res.addAll(classHierarchy(superC.next().property("class-name").value().toString()));
            }
            
            // add all implemented interfaces, sorted by their index
            Iterator<Edge> superI = db.traversal().V(clsV).outE("implements");
            List<Edge> esupers = new ArrayList<>();
            while (superI.hasNext())
            {
               esupers.add(superI.next());
            }
            
            esupers.sort(new Comparator<Edge>()
                         {
                             @Override
                             public int compare(Edge e1, Edge e2)
                             {
                                int idx1 = (int) property(e1, "implements-index");
                                int idx2 = (int) property(e2, "implements-index");
                                
                                return idx1 - idx2;
                             }
                          });
            
            superI = esupers.iterator();
            while (superI.hasNext())
            {
               Edge esuper = superI.next();
               
               res.addAll(classHierarchy(esuper.inVertex().property("class-name").value().toString()));
            }
            
            if ((int) property(clsV, "node-type") == CLASS_DEF)
            {
               res.add(SymbolResolver.ROOT_OBJ_NAME);
            }
         }
         
         return res;
      }
      
      /**
       * Create an iterator for all the nodes linked with an outbound edge which has a property 
       * with the specified key and value.
       *
       * @param    node
       *           The graph node.
       * @param    key
       *           The property key.
       * @param    value
       *           The property value.
       * 
       * @return   See above.
       */
      public Iterator<Vertex> nodeIterator(Vertex node, String key, Object value)
      {
         return db.traversal().V(node).outE().has(key, value).inV();
      }
      
      /**
       * Create an iterator for all the nodes linked with an outbound edge with the given label 
       * and the target node having the given property value.
       *
       * @param    node
       *           The graph node.
       * @param    label
       *           The edge label.
       * @param    key
       *           The property key.
       * @param    value
       *           The property value.
       * @param    out
       *           The direction: out or in edges.
       * 
       * @return   See above.
       */
      public Iterator<Vertex> nodeIterator(Vertex  node,
                                           String  label,
                                           String  key,
                                           Object  value, 
                                           boolean out)
      {
         return out ? db.traversal().V(node).outE(label).inV().has(key, value)
                    : db.traversal().V(node).inE(label).outV().has(key, value);
      }
      
      /**
       * Create an iterator for all the nodes linked with an outbound edge, which have a property 
       * with the specified key and value.
       *
       * @param    node
       *           The graph node.
       * @param    key
       *           The property key.
       * @param    value
       *           The property value.
       * 
       * @return   See above.
       */
      public Iterator<Vertex> nodeIteratorV(Vertex node, String key, Object value)
      {
         return db.traversal().V(node).outE().inV().has(key, value);
      }
      
      /**
       * Create an iterator for all the nodes having a property with the specified key, regardless
       * of value.
       * 
       * @param    key
       *           The property key.
       * 
       * @return   See above.
       */
      public Iterator<Vertex> nodeIterator(String key)
      {
         return db.traversal().V().has(key);
      }

      /**
       * Create a BFS node iterator starting with the given root, in the given direction.
       * 
       * @param    root
       *           The root node.
       * @param    dir
       *           The edge direction ({@code true} for out and {@code false} for in.
       *           
       * @return   See above.
       */
      public Iterator<Vertex> traversalIterator(Vertex root, final boolean dir)
      {
         // start with our root
         final LinkedList<Vertex> targets = new LinkedList<>();
         targets.add(root);
         final Set<Vertex> traversed = new HashSet<Vertex>();
         traversed.add(root);
         
         return new Iterator<Vertex>()
         {
            @Override
            public void remove()
            {
               throw new UnsupportedOperationException();
            }
            
            @Override
            public Vertex next()
            {
               Vertex next = targets.removeFirst();
               
               // collect all edges in the given direction
               GraphTraversal<?, Vertex> gv   = db.traversal().V(next);
               Iterator<Vertex>          iter = dir ? gv.outV() : gv.inV();
               
               while (iter.hasNext())
               {
                  Vertex node = iter.next();
                  
                  if (!traversed.contains(node))
                  {
                     targets.addLast(node);
                     traversed.add(node);
                  }
               }
               
               return next;
            }
            
            @Override
            public boolean hasNext()
            {
               return !targets.isEmpty();
            }
         };
      }

      /**
       * Create a BFS node iterator starting with the given root.
       * 
       * @param    root
       *           The root node.
       * @param    label
       *           The labels for the edges which will be followed.
       *           
       * @return   See above.
       */
      public Iterator<Vertex> traversalIterator(Vertex root, final String label)
      {
         return traversalIterator(root, label, true);
      }
      
      /**
       * Create a BFS node iterator starting with the given root.
       * 
       * @param    root
       *           The root node.
       * @param    label
       *           The labels for the edges which will be followed.
       * @param    dir
       *           The edge direction ({@code true} for out and {@code false} for in.
       *           
       * @return   See above.
       */
      public Iterator<Vertex> traversalIterator(Vertex root, final String label, boolean dir)
      {
         // start with our root
         final LinkedList<Vertex> targets = new LinkedList<>();
         targets.add(root);
         final Set<Vertex> traversed = new HashSet<Vertex>();
         traversed.add(root);
         
         return new Iterator<Vertex>()
         {
            @Override
            public void remove()
            {
               throw new UnsupportedOperationException();
            }
            
            @Override
            public Vertex next()
            {
               Vertex next = targets.removeFirst();
               
               // collect all outbound edges with the given label
               GraphTraversal<?, Vertex> gv = db.traversal().V(next);
               Iterator<Vertex>        iter = dir ? gv.outE(label).inV() : gv.inE(label).outV();
               
               while (iter.hasNext())
               {
                  Vertex node = iter.next();
                  
                  if (!traversed.contains(node))
                  {
                     targets.addLast(node);
                     traversed.add(node);
                  }
               }
               
               return next;
            }
            
            @Override
            public boolean hasNext()
            {
               return !targets.isEmpty();
            }
         };
      }
      
      /**
       * Obtain a vertex traversal instance that starts at the given node.
       *
       * @param    v
       *           The node at which to start the traversal.
       *
       * @return   The traversal instance (which can also be used as an iterator).
       */
      public GraphTraversal<?, Vertex> vertexTraversal(Vertex v)
      {
         return db.traversal().V(v);
      }
      
      /**
       * Obtain a vertex traversal instance that starts at the given node, for IN edges.
       *
       * @param    v
       *           The node at which to start the traversal.
       *
       * @return   The traversal instance (which can also be used as an iterator).
       */
      public Iterator<Edge> vertexTraversalInE(Vertex v)
      {
         return db.traversal().V(v).inE();
      }
      
      /**
       * Create an iterator for all the nodes having a property with the specified key and value.
       * 
       * @param    key
       *           The property key.
       * @param    value
       *           The property value.
       * 
       * @return   See above.
       */
      public Iterator<Vertex> nodeIterator(String key, Object value)
      {
         return db.traversal().V().has(key, value);
      }
      
      /**
       * Create an iterator for all the nodes having a property with the specified key and value.
       * 
       * @param    key
       *           The property key.
       * @param    value
       *           The property value.
       * @param    key2
       *           The second property key.
       * @param    value2
       *           The second property value.
       * 
       * @return   See above.
       */
      public Iterator<Vertex> nodeIterator(String key, Object value, String key2, Object value2)
      {
         return db.traversal().V().has(key, value).has(key2, value2);
      }
      
      /**
       * Create an iterator for all the nodes having a property with the specified key and value,
       * and without the specified property.
       * 
       * @param    key
       *           The property key.
       * @param    value
       *           The property value.
       * @param    without
       *           The property name which must not be present at the vertex.
       *           
       * @return   See above.
       */
      public Iterator<Vertex> nodeIteratorWithout(String key, Object value, String without)
      {
         return db.traversal().V().has(key, value).hasNot(without);
      }

      /**
       * Get all non-virtual functions with the specified name.
       * 
       * @param    name
       *           The function name.
       * @param    adm
       *           Flag indicating if we need functions from ADM programs or non-ADM programs.
       *           
       * @return   A vertex iterator.
       */
      public Iterator<Vertex> functionsIterator(String name, boolean adm)
      {
         if (functions == null)
         {
            functions = loadInternalEntries(true);
         }
         
         List<Object>[] funcs = functions.get(name.toLowerCase());
         return funcs == null ? Collections.emptyIterator() : vertexIterator(funcs[adm ? 0 : 1]);
      }

      /**
       * Get all non-virtual functions with the specified name.
       * 
       * @param    name
       *           The function name.
       * 
       * @return   A vertex iterator.
       */
      public Iterator<Vertex> functionsIterator(String name)
      {
         if (functions == null)
         {
            functions = loadInternalEntries(true);
         }
         
         List<Object>[] funcs = functions.get(name.toLowerCase());
         return funcs == null ? Collections.emptyIterator() : vertexIterator(funcs[2]);
      }
      
      /**
       * Get all non-virtual internal procedures with the specified name.
       * 
       * @param    name
       *           The procedure name.
       * @param    adm
       *           Flag indicating if we need functions from ADM programs or non-ADM programs.
       * 
       * @return   A vertex iterator.
       */
      public Iterator<Vertex> proceduresIterator(String name, boolean adm)
      {
         if (procedures == null)
         {
            procedures = loadInternalEntries(false);
         }
         
         List<Object>[] procs = procedures.get(name.toLowerCase());
         return procs == null ? Collections.emptyIterator() : vertexIterator(procs[adm ? 0 : 1]);
      }
      
      /**
       * Get all non-virtual internal procedures with the specified name.
       * 
       * @param    name
       *           The procedure name.
       * 
       * @return   A vertex iterator.
       */
      public Iterator<Vertex> proceduresIterator(String name)
      {
         if (procedures == null)
         {
            procedures = loadInternalEntries(false);
         }
         
         List<Object>[] procs = procedures.get(name.toLowerCase());
         return procs == null ? Collections.emptyIterator() : vertexIterator(procs[2]);
      }
      
      /**
       * Sort the given elements by the specified properties.
       * 
       * @param    elements
       *           A query result.
       * @param    props
       *           The property names, each element a {@link String}.
       *           
       * @return   The sorted elements.
       */
      @SuppressWarnings("unchecked")
      public Iterator<Element> sortedResults(Iterable<Element> elements, final Object[] props)
      {
         if (!elements.iterator().hasNext())
         {
            return Collections.EMPTY_LIST.iterator();
         }

         List<Element> sorted = new ArrayList<>();
         for (Element e : elements)
         {
            sorted.add(e);
         }
         
         Collections.sort(sorted, new Comparator<Element>()
         {
            @Override
            public int compare(Element o1, Element o2)
            {
               for (int i = 0; i < props.length; i++)
               {
                  Object v1 = o1.<Object>property((String) props[i]).value();
                  Object v2 = o2.<Object>property((String) props[i]).value();
                  
                  if (v1 != null && v1.equals(v2))
                  {
                     continue;
                  }
                  else if (v1 instanceof Comparable)
                  {
                     return ((Comparable<Object>) v1).compareTo(v2);
                  }
                  else
                  {
                     return 0;
                  }
               }
               
               return 0;
            }
         });
         
         return sorted.iterator();
      }
      
      /**
       * Create an iterator for all outbound edges with the given label.
       * 
       * @param    node
       *           The node for which the outbound edges are needed.
       * @param    label
       *           The edge's label.
       *           
       * @return   See above.
       */
      public Iterator<Edge> edgeIterator(Vertex node, String label)
      {
         return db.traversal().V(node).outE(label);
      }

      /**
       * Create an iterator for all outbound edges with the given label and property key and value.
       * 
       * @param    node
       *           The node for which the outbound edges are needed.
       * @param    label
       *           The edge's label.
       * @param    key
       *           Only edges with the having a property with this key.
       * @param    value
       *           Only edges with the having a property with this value.
       *           
       * @return   See above.
       */
      public Iterator<Edge> edgeIterator(Vertex node, String label, String key, Object value)
      {
         return db.traversal().V(node).outE(label).has(key, value);
      }

      /**
       * Create an iterator for all inbound edges with the given label.
       * 
       * @param    node
       *           The node for which the inbound edges are needed.
       * @param    label
       *           The edge's label.
       *           
       * @return   See above.
       */
      public Iterator<Edge> edgeInIterator(Vertex node, String label)
      {
         return db.traversal().V(node).inE(label);
      }
      
      /**
       * Find a node by its type and ID.
       * 
       * @param    nodeType
       *           The node's type.
       * @param    id
       *           The node's ID.
       *
       * @return   The found node or {@code null} if it does not exist.
       */
      public Vertex findNodeById(int nodeType, Object id)
      {
         return CallGraphHelper.findNodeById(nodeType, id);
      }
      
      /**
       * Find the AST vertex in the {@link ProgressParserTokenTypes#AST_NODE} resource domain.
       * 
       * @param    id
       *           The node-id property value to match.
       *           
       * @return   The found vertex or <code>null</code>.
       */
      public Vertex findAstNode(long id)
      {
         Iterator<Vertex> iter = db.traversal().V().has("node-id", id)
                                                   .has("resource-domain", AST_NODE)
                                                   .limit(1);
         return iter.hasNext() ? iter.next() : null;
      }

      /**
       * Find the adjacent vertex to the uniquely described vertex, via a unique edge
       * matching the given label.
       * 
       * @param    type
       *           The "node-type" property.
       * @param    id
       *           The "node-id" property.
       * @param    label
       *           The label of the edge defining the relation between the two vertices.
       * @param    dir
       *           {@code true} for out edges and {@code false} for in edges.
       *
       * @return   The found node or {@code null} if it does not exist.
       */
      public Vertex findUniqueAdjacent(int type, Object id, String label, boolean dir)
      {
         Vertex node = CallGraphHelper.findNodeById(type, id);
         
         Vertex result = null;
         
         if (node != null)
         {
            GraphTraversal<?, Vertex> g = db.traversal().V(node);
            
            Iterator<Vertex> iter = (dir ? g.out(label) : g.in(label)).limit(2);
            
            while (iter.hasNext())
            {
               if (result == null)
               {
                  result = iter.next();
               }
               else
               {
                  throw new RuntimeException("More than 1 containing node found!");
               }
            }
         }
         
         return result;
      }
      
      /**
       * Get the first adjacent node on the given direction, with the specified type and edge
       * label.
       * 
       * @param    node
       *           The current node.
       * @param    type
       *           The found "node-type" property.
       * @param    label
       *           The label of the edge defining the relation between the two vertices.
       * @param    dir
       *           {@code true} for out edges and {@code false} for in edges.
       *
       * @return   First found node or {@code null} if it does not exist.
       */
      public Vertex findAdjacent(Vertex node, int type, String label, boolean dir)
      {
         return CallGraphHelper.findAdjacent(node, type, label, dir);
      }
      
      /**
       * Get the first adjacent node on the given direction, with the specified type and edge
       * label.
       * 
       * @param    node
       *           The current node.
       * @param    type
       *           The found "node-type" property.
       * @param    label
       *           The label of the edge defining the relation between the two vertices.
       * @param    prop
       *           The property name.
       * @param    val
       *           The property value.
       * @param    dir
       *           {@code true} for out edges and {@code false} for in edges.
       *
       * @return   First found node or {@code null} if it does not exist.
       */
      public Vertex findAdjacent(Vertex  node,
                                 int     type,
                                 String  label,
                                 String  prop,
                                 Object  val,
                                 boolean dir)
      {
         GraphTraversal<?, Vertex> g = db.traversal().V(node);
         Iterator<Vertex> iter = dir ? g.out(label).has(prop, val).has("node-type", type).limit(1)
                                     : g.in(label).has(prop, val).has("node-type", type).limit(1);
         
         return (iter.hasNext() ? iter.next() : null);
      }

      /**
       * Add a new target, to be processed.
       * <p>
       * Only {@code external-procedure} targets with no {@code finished} or {@code processing}
       * annotations are added.
       * 
       * @param    node
       *           The node to be added.
       */
      public void addTarget(Vertex node)
      {
         if (!collectTargets || node == null)
         {
            return;
         }
         
         Vertex vproc = null;
         
         // add only AST targets and skip finished IDs and WIP IDs
         if (node.<Integer>property("node-type").value().equals(EXTERNAL_PROCEDURE))
         {
            vproc = findUniqueAdjacent(EXTERNAL_PROCEDURE, 
                                       node.property("node-id").value(), 
                                       "contains", 
                                       false);
         }
         else if (node.<Integer>property("node-type").value().equals(PROCEDURE_FILE))
         {
            vproc = node;
         }
         
         if (vproc != null)
         {
            if (!vproc.<Boolean>property("finished").isPresent() && 
                !vproc.<Boolean>property("processing").isPresent())
            {
               targets.addLast(vproc);
               vproc.<Boolean>property("processing", true);
            }
         }
      }
      
      /**
       * Commit the current transaction.
       * <p>
       * If the number of {@link #txNewVertices}, {@link #txNewEdges} plus {@link #txChangedProperties}
       * is over 10000, then we force a commit.
       * 
       * @param    force
       *           Flag indicating if we force the commit.
       */
      public void commit(boolean force)
      {
         if (!force && txNewVertices + txNewEdges + txChangedProperties > 10000)
         {
            force = true;
         }
         
         if (force)
         {
            // close and open the tx so that caches are evicted.

            db.tx().commit();
            db.tx().close();

            db.tx().open();

            txNewVertices = 0;
            txNewEdges = 0;
            txChangedProperties = 0;
         }
      }
      
      /**
       * Commit the current transaction.
       */
      public void commit()
      {
         commit(true);
      }
      
      /**
       * Return all class vertices with the qualified class name.
       * 
       * @param    className
       *           The qualified class name.
       * @return   An iterator with the found vertices.
       */
      public Iterator<Vertex> findClassNode(String className)
      {
         return CallGraphHelper.findClassNode(className);
      }
      
      /**
       * Find an unique node having a property with the given key and value.
       * 
       * @param    key
       *           The property key.
       * @param    val
       *           The property value.
       *           
       * @return   See above.
       * 
       * @throws   RuntimeException
       *           If more than one node is found.
       */
      public Vertex findUniqueNode(String key, Object val)
      {
         return CallGraphHelper.findUniqueNode(key, val);
      }
      
      /**
       * Find the parent vertex having the given type.
       * 
       * @param    node
       *           The node to find.
       * @param    type
       *           The parent's type.
       *           
       * @return   See above.
       */
      public Vertex findParentAstVertex(Vertex node, int type)
      {
         return CallGraphHelper.findParentAstVertex(node, type);
      }
      
      /**
       * Find the procedure file associated with the given AST node.
       * 
       * @param    node
       *           The node to find.
       *           
       * @return   See above.
       */
      public Vertex findProcedureFile(Vertex node)
      {
         return CallGraphHelper.findProcedureFile(node);
      }

      /**
       * Load all the include files (physical files) into the graph DB, as distinct nodes. This
       * ensures that unreferenced/dead include files can be determined.
       */
      public void loadIncludeFiles()
      {
         long start = System.currentTimeMillis();
         
         String basepath = Configuration.getParameter("basepath", "./");
         String spec     = Configuration.getParameter("include-spec", "*.[iI]");
         
         System.out.println(String.format("Using %s for include-file specifications.", spec));
         
         FileSpecList list     = new FileSpecList(new File(basepath), spec, true);
         File[]       includes = list.list();
         
         for (File fi : includes)
         {
            String filename = fi.getPath();
            System.out.println(filename);
            Vertex vnode = CallGraphHelper.findUniqueNode("os-filename", filename);
            if (vnode == null)
            {
               createIncludeFile(filename);
            }
         }
         
         // commit the remainder transaction
         commit();
         
         long elapsed = System.currentTimeMillis() - start;
         Date date = new Date(elapsed - TimeZone.getDefault().getRawOffset());
         SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
         System.out.println("Done adding include-files in " + format.format(date));
      }

      /**
       * Create the include graph for the given external program.
       * 
       * @param    ast
       *           The external program AST.
       *           
       * @throws   PreprocessorException
       *           An unknown or unexpected failure.
       */
      public void createIncludeGraph(Aast ast)
      throws PreprocessorException
      {
         PreprocessorHints ppHints = loadPreprocessorHints(ast);

         HintsTreeNode root = ppHints.getRoot();
         if (root != null)
         {
            // find the procedure file which links to each include file that is referenced
            Vertex parent = findUniqueAdjacent(EXTERNAL_PROCEDURE, ast.getId(), "contains", false);
            
            createIncludeGraph(parent.<Long>property("node-id").value(), null, root, parent);
         }
      }

      /**
       * Get the targets for a specific call-site (which is determined to be ambiguous), 
       * from the hints file.
       * 
       * @param    callSite
       *           The call site.
       * @param    callSiteKey
       *           The key of this call-site.
       * @param    instance
       *           The instance number for this call-site.
       * 
       * @return   A map of targets, where the key represents the hint no (and the index in the
       *           string array) for the target.
       */
      public Map<CallSiteHint, String> getCallSiteHintTargets(Vertex callSite, 
                                                              int    callSiteKey, 
                                                              int    instance)
      {
         String lastUsedHints = (String) property(callSite, "last-used-hints");

         Map<CallSiteHint, String> includeHints = 
                                      getCallSiteIncludeHintTargets(callSite, callSiteKey);
         
         Map<CallSiteHint, String> res = getCallSiteHintTargets(hints, 
                                                                callSiteKey,
                                                                instance,
                                                                false);
         
         // if hints haven't changed, do nothing
         String currentHints = null;
         if (res == null)
         {
            currentHints = includeHints == null ? null : includeHints.toString();
         }
         else
         {
            currentHints = res.toString();
         }
         
         if (lastUsedHints == null && currentHints == null)
         {
            return null;
         }

         if (lastUsedHints != null && lastUsedHints.equals(currentHints))
         {
            return Collections.emptyMap();
         }
         
         if (res != null && includeHints != null)
         {
            // delete all previously 'include hints' edges
            for (CallSiteHint csh : includeHints.keySet())
            {
               db.traversal().V(callSite)
                 .outE()
                 .has("included-hint", true)
                 .has("hint-id", csh.toString())
                 .forEachRemaining(e -> e.remove());
            }
         }
         
         res = res != null ? res : includeHints;
         
         if (res != null)
         {
            property(callSite, "last-used-hints", res.toString());
         }
         else if (lastUsedHints != null)
         {
            removeProperty(callSite, "last-used-hints");
         }
         
         return res;
      }

      /**
       * Resolve all the external programs which match the given file name.
       * 
       * @param    name
       *           The filename.
       *           
       * @return   See above.
       */
      public List<String> resolveExternalPrograms(String name)
      {
         Set<String> res = new TreeSet<>();

         boolean hasAliases = pathAliases != null;
         boolean caseSens = isCaseSensitive();
         
         String filename = CallGraphHelper.prepareFilename(name);
         java.util.function.Function<String, Boolean> isRcode = 
                  (pname -> pname.endsWith(".r") || (!caseSens && pname.endsWith(".R")));
         boolean legacyRcode = isRcode.apply(filename);

         // TODO: RUN can target r-code even if extension is missing; i.e. RUN foo. will execute
         // foo.r, if it exists

         Consumer<String> lookupDirectName;
         Consumer<String> lookupCompiledRcode;
         Consumer<String> lookupAliasedDirectName;
         Consumer<String> lookupAliasedCompiledRcode;
        
         // we can't search in the propath and maybe resolve an unique name... the runtime propath
         // is unknown at this time, thus resolve all matches
         
         // instead of looking for a suffix, use the "reverse-filename" property to look for a 
         // prefix, which is indexed properly by then graph DB.

         lookupDirectName = pname ->
         {
            // append a "/" prefix so that the name will match in any folder
            String search = "/" + pname;
            search = reverseString(search);
            
            db.traversal().V().has("reverse-filename", textPrefix(search)).forEachRemaining(v -> 
            {
               res.add((String) v.<String>property("filename").value());
            });
         };
         lookupCompiledRcode = !legacyRcode ? null : (pname ->
         {
            if (!isRcode.apply(pname))
            {
               return;
            }
            
            // append a "/" prefix so that the name will match in any folder
            String search = "/" + pname;
            search = reverseString(search);
            
            db.traversal().V()
              .has("reverse-compiled-filename", textPrefix(search))
              .forEachRemaining(v -> 
            {
               res.add((String) v.<String>property("filename").value());
            });
         });
         lookupAliasedDirectName = !hasAliases ? null : (pname ->
         {
            String[] aliases = SourceNameMapper.resolvePathAliases(pathAliases, pname);
            
            for (String aliasName : aliases)
            {
               lookupDirectName.accept(aliasName);
            }
         });
         lookupAliasedCompiledRcode = !legacyRcode || !hasAliases ? null : (pname ->
         {
            if (!isRcode.apply(pname))
            {
               return;
            }
            
            String[] aliases = SourceNameMapper.resolvePathAliases(pathAliases, pname);
            
            for (String aliasName : aliases)
            {
               lookupCompiledRcode.accept(aliasName);
            }
         });

         @SuppressWarnings("unchecked")
         Consumer<String>[] lookups = new Consumer[]
         {
            lookupDirectName,
            lookupCompiledRcode,
            lookupAliasedDirectName,
            lookupAliasedCompiledRcode,
         };
         
         for (Consumer<String> lookup : lookups)
         {
            if (lookup == null)
            {
               continue;
            }

            lookup.accept(filename);
         }
         
         if (res.isEmpty())
         {
            // if not found, add it, as it will be marked as missing
            res.add(name);
         }
         
         return new ArrayList<>(res);
      }
      
      /**
       * Print output to the terminal if debug settings allow.
       *
       * @param    text
       *           Text to print.
       */
      public void println(String text)
      {
         if (!silent)
         {                                                            
            System.out.println(text);
         }
      }

      /**
       * Loads the hints file for an AST if it exists and hasn't already been loaded.
       *
       * @return   <code>true</code> if there are hints for this file and the hints are loaded, 
       *           <code>false</code> if no hints are available.
       */
      public boolean loadHints()
      {
         if (hints == null)
         {
            try
            {
               Aast src = getResolver().getSourceAst();
               File file = Configuration.toFile(src.getFilename());

               hints = new UastHints(file.getAbsolutePath());
            }

            catch (Exception exc)
            {
               hints = null;
               
               // any failure drops us out
               return false;
            }
         }

         return true;
      }

      /**
       * Create an include file vertex for the given filename.  This will also check if there is
       * an external program file already parsed, with the same filename.  If so, it will create
       * a program file, not an include file.
       *  
       * @param    filename
       *           The name of the file resource.
       */
      public Vertex createIncludeFile(String filename)
      {
         try
         {
            AstManager.get().loadTree(filename + ".ast");
            
            // this is a program file
            Vertex vProg = findUniqueNode("os-filename", filename);
            if (vProg == null)
            {
               vProg = createFileResource("Procedure File", PROCEDURE_FILE, filename);
            }
            
            return vProg;
         }
         catch (Exception e) 
         {
            // ignore this
         }
         
         // it is a real include file
         return createFileResource("Include File", INCLUDE_FILE, filename);
      }

      /**
       * Process this call site and compute the details for defining include-level hints.
       * 
       * @param    callSite
       *           The call site.
       * @param    callSiteKey
       *           The key of this call-site.
       */
      public void setIncludeHints(Vertex callSite, int callSiteKey)
      {
         if (!callSite.<Boolean>property("included").value() || 
             callSite.property("include-hint-file").isPresent())
         {
            return;
         }
      
         // find the include file for this callSite
         Iterator<Vertex> viter = db.traversal().V(callSite).outE("includes").inV();
         Vertex includeRef = viter.hasNext() ? viter.next() : null;
         if (includeRef == null)
         {
            return;
         }
         
         if (viter.hasNext())
         {
            // TODO: more than one include?
         }
         
         String includeFilename = includeRef.<String>property("filename").value();

         Map<Integer, AtomicInteger> includeHintIds = includeHintsNextIds.get(includeFilename);
         if (includeHintIds == null)
         {
            includeHintsNextIds.put(includeFilename, includeHintIds = new HashMap<>());
         }
         AtomicInteger nextId = includeHintIds.get(callSiteKey);
         if (nextId == null)
         {
            includeHintIds.put(callSiteKey, nextId = new AtomicInteger(0));
         }
         int hintId = nextId.getAndIncrement();
         property(callSite, "include-hint-file", includeFilename);
         property(callSite, "include-hint-instance", hintId);
      }

      /**
       * Load the include-level hints for the given include file.
       * 
       * @param    filename
       *           The include file name.
       *           
       * @return   The loaded hints or <code>null</code> if non exists.
       */
      private UastHints loadIncludeHints(String filename)
      {
         UastHints hints = includeHints.get(filename);
         if (hints != null)
         {
            return hints;
         }
         
         try
         {
            File file = Configuration.toFile(filename);
            hints = new UastHints(file.getAbsolutePath());
         }
         catch (Exception exc)
         {
            return null;
         }
         
         includeHints.put(filename, hints);
         
         return hints;
      }

      /**
       * Load the associated include-level hints for this call site.
       * 
       * @param    callSite
       *           The call site.
       * @param    callSiteKey
       *           The key of this call-site.
       * 
       * @return   A map with the hints for this call site.
       */
      private Map<CallSiteHint, String> getCallSiteIncludeHintTargets(Vertex callSite, 
                                                                      int    callSiteKey)
      {
         if (!callSite.<Boolean>property("included").value())
         {
            return null;
         }
         
         int includeHintInstance = callSite.<Integer>property("include-hint-instance").value();
         String includeHintFile = callSite.<String>property("include-hint-file").value();
         
         UastHints includeHints = loadIncludeHints(includeHintFile);
         if (includeHints == null)
         {
            return null;
         }
         
         return getCallSiteHintTargets(includeHints, callSiteKey, includeHintInstance, true);
      }

      /**
       * Get the targets for a specific call-site (which is determined to be ambiguous), 
       * from the hints file.
       * 
       * @param    hints
       *           The hints to process.
       * @param    callSiteKey
       *           The key of this call-site.
       * @param    instance
       *           The instance number for this call-site.
       * @param    include
       *           Flag indicating this is from an include file.
       *           
       * @return   A map of targets, where the key represents the hint no (and the index in the
       *           string array) for the target.
       */
      private Map<CallSiteHint, String> getCallSiteHintTargets(UastHints hints,
                                                               int       callSiteKey, 
                                                               int       instance,
                                                               boolean   include)
      {
         if (hints == null)
         {
            return null;
         }

         String suffixSep = ":";
      
         StringBuilder sb = new StringBuilder(ProgressParser.lookupTokenName(callSiteKey));
         sb.append("_");
         sb.append(Integer.toString(instance));
         
         String prefix = sb.toString();
         String[] hintKeys = hints.getUastHints(prefix);
         
         if (hintKeys == null || hintKeys.length == 0)
         {
            return null;
         }
         
         Map<CallSiteHint, String> res = new LinkedHashMap<>();
         
         for (String key : hintKeys)
         {
            String cls = hints.getUastType(key);
            if (String.class.getName().equals(cls))
            {
               String target = hints.getUastString(key);
               
               String suffix = null;
               int sufIdx = target.indexOf(suffixSep);
               if (sufIdx > 0)
               {
                  suffix = target.substring(sufIdx + 1);
                  target = target.substring(0, sufIdx);
                  
                  if (suffix.isEmpty())
                  {
                     suffix = null;
                  }
               }
               
               List<String> extProgs = resolveExternalPrograms(target);
               for (int j = 0; j < extProgs.size(); j++)
               {
                  String extProg = extProgs.get(j);
                  if (extProgs.size() == 1)
                  {
                     res.put(new CallSiteHint(instance, -1, -1, suffix, include), target.trim());
                  }
                  else
                  {
                     res.put(new CallSiteHint(instance, -1, j, suffix, include), extProg);
                  }
               }
            }
            else
            {         
               // second try, maybe the hint is an array (we will get an exception if this isn't 
               // the case... this is OK)
               String[] targets = hints.getUastStringArray(key);
               
               // process each entry in the array
               for (int i = 0; i < targets.length; i++)
               {
                  String target = targets[i];
      
                  String suffix = null;
                  int sufIdx = target.indexOf(suffixSep);
                  if (sufIdx > 0)
                  {
                     suffix = target.substring(sufIdx + 1);
                     target = target.substring(0, sufIdx);
                     
                     if (suffix.isEmpty())
                     {
                        suffix = null;
                     }
                  }
      
                  List<String> extProgs = resolveExternalPrograms(target);
                  for (int j = 0; j < extProgs.size(); j++)
                  {
                     String extProg = extProgs.get(j);
                     if (extProgs.size() == 1)
                     {
                        res.put(new CallSiteHint(instance, i, -1, suffix, include), extProg);
                     }
                     else
                     {
                        res.put(new CallSiteHint(instance, i, j, suffix, include), extProg);
                     }
                  }
               }
            }
         }
         
         return res;
      }

      /**
       * Load all non-virtual internal entries.
       * 
       * @param    function
       *           Flag indicating if we want functions or internal procedures.
       * 
       * @return   A map of function/procedure names as keys and the associated IDs, for each
       *           name.
       */
      private Map<String, List<Object>[]> loadInternalEntries(boolean function)
      {
         // 0 - ADM files, 1 - normal files
         Map<String, List<Object>[]> res = new HashMap<>();
         String prop = function ? "function" : "procedure";

         Iterator<Vertex> iter = db.traversal().V().has("node-key", prop).hasNot("virtual");
         while (iter.hasNext())
         {
            Vertex v = iter.next();
            
            String name = (String) property(v, prop);
            List<Object>[] list = res.get(name);
            if (list == null)
            {
               res.put(name, list = new LinkedList[3]);
               for (int i = 0; i < list.length; i++)
               {
                  list[i] = new LinkedList<>();
               }
            }
            
            Vertex vp = findProcedureFile(v);
            List<Object> target = vp.property("adm-program").isPresent() ? list[0] : list[1];
            target.add(v.id());
            list[2].add(v.id());
         }
         
         return res;
      }
      
      /**
       * Obtain a vertex iterator over the specified vertex IDs.
       * 
       * @param    vertexIds
       *           The vertex IDs.
       *  
       * @return   See above.
       */
      private Iterator<Vertex> vertexIterator(List<Object> vertexIds)
      {
         if (vertexIds.isEmpty())
         {
            return Collections.emptyIterator();
         }
      
         Object[] vids = vertexIds.toArray();
         return db.traversal().V(vids).iterate();
      }

      /**
       * Cache the possible top-level block parents for this AST node.
       * <p>
       * The parent vertex ID is cached in the <code>parent-node-type-[vertex-id]</code> property
       * at the vertex.
       * 
       * @param    ast
       *           The AST node.
       * @param    node
       *           The created vertex node.
       */
      private void cacheParents(Aast ast, Vertex node)
      {
         boolean isSchema = ast.ancestor(-1, DATABASE) || ast.getType() == DATABASE;
         int[] ancestors = null;
         int nodeType = node.<Integer>property("node-type").value();
         
         if (isSchema)
         {
            ancestors = new int[]
            {
               TABLE,
               DATABASE
            };
         }
         else
         {
            ancestors = new int[]
            {
               KW_ON,
               TRIGGER_BLOCK,
               FUNCTION,
               INTERNAL_PROCEDURE,
               METHOD_DEF,
               DEFINE_PROPERTY_SET,
               DEFINE_PROPERTY_GET,
               CONSTRUCTOR,
               DESTRUCTOR,
               CLASS_DEF,
               INTERFACE_DEF,
               ENUM_DEF
            };
         }

         boolean linkToExternal = true;
         for (int ancestorType : ancestors)
         {
            if (ancestorType == nodeType)
            {
               linkToExternal = false;
               continue;
            }
            
            int atype = ancestorType;
            switch (ancestorType)
            {
               case KW_ON:
                  atype = KW_ON;
                  break;
               case TRIGGER_BLOCK:
                  atype = TRIGGER_BLOCK;
                  break;
               case FUNCTION:
                  atype = FUNCTION;
                  break;
               case INTERNAL_PROCEDURE:
                  atype = PROCEDURE;
                  break;
               case METHOD_DEF:
                  atype = METHOD_DEF;
                  break;
               case DEFINE_PROPERTY_SET:
                  atype = DEFINE_PROPERTY;
                  break;
               case DEFINE_PROPERTY_GET:
                  atype = DEFINE_PROPERTY;
                  break;
               case CONSTRUCTOR:
                  atype = CONSTRUCTOR;
                  break;
               case DESTRUCTOR:
                  atype = DESTRUCTOR;
                  break;
               case CLASS_DEF:
                  atype = CLASS_DEF;
                  break;
               case INTERFACE_DEF:
                  atype = INTERFACE_DEF;
                  break;
               case ENUM_DEF:
                  atype = ENUM_DEF;
                  break;
            }

            Aast ancestor = ast.getAncestor(-1, atype);

            if (ancestor == null)
            {
               continue;
            }
            
            switch (ancestorType)
            {
               case KW_ON:
                  // nothing to do
                  break;
               case TRIGGER_BLOCK:
                  // nothing to do
                  break;
               case FUNCTION:
                  ancestor = ancestor.getImmediateChild(KW_FUNCT, null);
                  break;
               case INTERNAL_PROCEDURE:
                  ancestor = ancestor.getImmediateChild(KW_PROC, null);
                  break;
               case METHOD_DEF:
                  ancestor = ancestor.getImmediateChild(KW_METHOD, null);
                  break;
               case DEFINE_PROPERTY_SET:
                  // nothing to do
                  break;
               case DEFINE_PROPERTY_GET:
                  // nothing to do
                  break;
               case CONSTRUCTOR:
                  ancestor = ancestor.getImmediateChild(KW_CONSTRUC, null);
                  break;
               case DESTRUCTOR:
                  ancestor = ancestor.getImmediateChild(KW_DESTRUCT, null);
                  break;
               case CLASS_DEF:
                  ancestor = ancestor.getImmediateChild(KW_CLASS, null);
                  break;
               case INTERFACE_DEF:
                  ancestor = ancestor.getImmediateChild(KW_INTERFAC, null);
                  break;
               case ENUM_DEF:
                  ancestor = ancestor.getImmediateChild(KW_ENUM, null);
                  break;
            }
            
            Iterator<Vertex> iter = db.traversal().V()
                                      .has("node-id", ancestor.getId())
                                      .has("node-type", ancestorType)
                                      .limit(1);

            if (!iter.hasNext())
            {
               continue;
            }
            
            Vertex parent = iter.next();
            if (parent != null)
            {
               String parentLabelName = CallGraphHelper.parentProperty(ancestorType);
               property(node, parentLabelName, parent.id());

               linkToExternal = false;
            }
         }
         
         if (isSchema)
         {
            findParentAstVertex(node, SCHEMA_FILE);
         }
         else
         {
            if (linkToExternal)
            {
               // external procedure code is in its separate 'container', so use that if no
               // other parent was found
               Aast ancestor = ast.getAncestor(-1);
               Vertex parent = db.traversal().V()
                                 .has("node-id", ancestor.getId())
                                 .has("node-type", EXTERNAL_PROCEDURE)
                                 .limit(1)
                                 .next();
   
               if (parent != null)
               {
                  String parentLabelName = CallGraphHelper.parentProperty(EXTERNAL_PROCEDURE);
                  property(node, parentLabelName, parent.id());
               }
            }
            
            Vertex root = db.traversal().V()
                            .has("filename", CallGraphHelper.prepareFilename(ast.getFilename()))
                            .next();
            String parentLabelName = CallGraphHelper.parentProperty(PROCEDURE_FILE);
            property(node, parentLabelName, root.id());
         }
      }
      
      /**
       * Loads the preprocessor hints file for an AST if it exists and hasn't already been loaded.
       *
       * @return   The loaded {@link PreprocessorHints} instance.
       *           
       * @throws   PreprocessorException
       *           An unknown or unexpected failure.
       */
      private PreprocessorHints loadPreprocessorHints(Aast ast)
      throws PreprocessorException
      {
         if (pphints == null)
         {
            pphints = new PreprocessorHints(ast.getFilename());
         }
         else
         {
            if (!ast.getFilename().equals(pphints.getSource()))
            {
               return new PreprocessorHints(ast.getFilename());
            }
         }
         
         return pphints;
      }

      /**
       * Create the include graph by walking the include tree specified by the root, recursively.
       * 
       * @param    fileSourceId
       *           The ID of the {@link ProgressParserTokenTypes#FILE_RESOURCE} vertex which
       *           started this include sub-graph.
       * @param    parentEdge
       *           The parent edge on which this include graph was created.  May be {@code null}
       *           if the {@code root} hints is on depth 0.
       * @param    root
       *           The root include node.
       * @param    parent
       *           The parent node in the graph DB, to which the root's children will be attached.
       */
      private void createIncludeGraph(long          fileSourceId,
                                      Edge          parentEdge,
                                      HintsTreeNode root, 
                                      Vertex        parent)
      {
         @SuppressWarnings("unchecked")
         Iterator<HintsTreeNode> iter = (Iterator<HintsTreeNode>) root.childIterator();
         while (iter.hasNext())
         {
            HintsTreeNode node = iter.next();
            IncludeHint hint = node.getIncludeHint();
            
            String hintfile = hint.getFilename();

            Vertex vnode = CallGraphHelper.findUniqueNode("os-filename", hintfile);
            
            if (vnode == null)
            {
               final String msg = "WARNING: creating a new node for the include file %s, which " +
                                  "was not found in initial include-file list.";
               System.out.println(String.format(msg, hint.getFilename()));

               // create it
               vnode = createIncludeFile(hintfile);
            }
            
            Edge edge = createEdge(parent, vnode, "includes");
      
            // copy the info: this needs to be set at the edge, as the same include file may
            // appear multiple times
            edge.<Integer>property("start-column", hint.getStartColumn());
            edge.<Integer>property("end-column",   hint.getEndColumn());
            edge.<Integer>property("start-line",   hint.getStartLine());
            edge.<Integer>property("end-line",     hint.getEndLine());
            
            edge.<Integer>property("column", hint.getStartColumn());
            edge.<Integer>property("line", hint.getStartLine());
            edge.<Long>property("call-site-id", fileSourceId);
            edge.<Integer>property("call-site-key", INCLUDES);
            if (parentEdge != null)
            {
               edge.<String>property("parent-edge-id", parentEdge.id().toString());
            }

            createIncludeGraph(fileSourceId, edge, node, vnode);
         }
      }
      
      /**
       * Compute the next available ID for an external node. External nodes are targets which
       * are not part of the code set ASTs (e.g. native processes, OCX controls, web service 
       * connections).
       * 
       * @param    type
       *           The external node type.
       *           
       * @return   The next available ID.
       */
      private long nextExternalId(int type)
      {
         // when no vertex is found, the returned value is MIN_LONG - that's why we always 
         // increment

         AtomicLong next = externalIds.get(type);
         if (next != null)
         {
            return next.incrementAndGet();
         }
         
         // all external IDs are negative
         GraphTraversal<Vertex, Comparable> maxNodeId = db.traversal()
           .V()
           .has("node-type", type)
           .values("node-id")
           .max();
           
         Comparable<Long> result = null;
         next = new AtomicLong();
         if (maxNodeId.hasNext())
         {
            result = maxNodeId.next();
            if (result != null && result instanceof Long)
            {
               next.set((Long) result + 1);
            }
         }
         externalIds.put(type, next);
         return next.get();
      }
   }
}