# CallGraphHelper.java

    /*
    ** Module   : CallGraphHelper.java
    ** Abstract : common support routines for call graph processing
    **
    ** Copyright (c) 2005-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description---------------------------------
    ** 001 GES 20170531 Created initial version, with some code being moved here from other files.
    ** 002 CA  20180327 Added index support and other performance optimizations.
    ** 003 CA  20180413 Enabled db-level cache and disabled vertex fast-property.
    **                  Cache the vertex parents in a parent-node-type-<node-type> property to avoid
    **                  expensive computation.
    **                  Allow graph connection settings to be specified via cfg/callgraph.properties.
    **     CA  20180418 Fixed Windows OS detection.
    ** 004 CA  20181003 Added findCallSites - gets all the call sites parented by a given AST.
    ** 005 CA  20181123 Basic support for multiple classes with the same qualified names
    ** 006 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 007 TJD 20240208 Java 17 dependencies updates
    */

    /*
    ** 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.util.*;
    import java.util.logging.*;

    import com.goldencode.p2j.convert.*;
    import org.apache.commons.configuration2.*;

    import com.goldencode.ast.*;
    import com.goldencode.graphdb.*;
    import com.goldencode.p2j.cfg.Configuration;
    import com.goldencode.p2j.util.*;
    import com.goldencode.util.*;

    import org.janusgraph.core.*;
    import org.janusgraph.core.attribute.Text;
    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.apache.tinkerpop.gremlin.process.traversal.P.*;
    import static org.janusgraph.core.attribute.Text.*;

    /**
     * Common support routines for call graph processing.
     */
    public class CallGraphHelper
    implements ProgressParserTokenTypes
    {
       /** Default folder for the callgraph DB. */
       public static final String DEFAULT_CALLGRAPH_DB_FOLDER = "./callgraph/";

       /** Id of the node to which ambiguous call-sites link. */
       public static final long AMBIGUOUS_NODE_ID = -1;

       /** Logger. */
       private static final ConversionStatus LOG = ConversionStatus.get(CallGraphHelper.class);

       /** Case sensitive state of the file names. */
       private static Boolean caseSensitive;

       /** Check if we are converting on Windows OS (current or legacy system). */
       private static Boolean winOs;

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

       /**
        * 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 static boolean isCaseSensitive()
       {
          if (caseSensitive == null)
          {
             caseSensitive =
                "true".equalsIgnoreCase(Configuration.getParameter("case-sensitive", "false"));
          }

          return caseSensitive;
       }

       /**
        * Check if we are converting on Windows OS or legacy sources were ran on Windows.
        *
        * @return   See above.
        */
       public static boolean isWindowsOs()
       {
          if (winOs == null)
          {
             winOs = PlatformHelper.isUnderWindowsFamily() ||
                     "win32".equalsIgnoreCase(Configuration.getParameter("opsys"));
          }

          return winOs;
       }

       /**
        * Get the vertex property mapping the specified parent type.
        *
        * @param    type
        *           The parent type.
        *
        * @return   See above.
        */
       public static String parentProperty(int type)
       {
          return String.format("parent-node-type-%d", type);
       }

       /**
        * Find all call-sites parented by the specified AST.
        *
        * @param    ast
        *           The parent AST.
        *
        * @return   A map of call-site AST ids to their associated graph vertex.
        */
       public static Map<Long, Vertex> findCallSites(Aast ast)
       {
          Map<Long, Vertex> res = new HashMap<>();

          Vertex vp = CallGraphHelper.findUniqueNode("node-id", ast.getId());
          if (vp == null)
          {
             return res;
          }

          int nodeType = (int) CallGraphHelper.property(vp, "node-type");

          String parentLabelName = CallGraphHelper.parentProperty(nodeType);
          db.traversal().V()
            .has(parentLabelName, vp.id())
            .has("entry-point", false)
            .forEachRemaining(v ->
          {
             Long id = v.<Long>property("node-id").value();

             res.put(id, v);
          });

          return res;
       }

       /**
        * 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 static Vertex findAdjacent(Vertex node, int type, String label, boolean dir)
       {
          GraphTraversal<?, Vertex> g = db.traversal().V(node);
          Iterator<Vertex> iter = dir ? g.out(label).has("node-type", type).limit(1)
                                      : g.in(label).has("node-type", type).limit(1);

          return (iter.hasNext() ? iter.next() : null);
       }

       /**
        * Return all class vertices with the qualified class name.
        *
        * @param    className
        *           The qualified class name.
        * @return   An iterator with the found vertices.
        */
       public static Iterator<Vertex> findClassNode(String className)
       {
          List<Vertex> res = new ArrayList<>();

          Iterator<Vertex> iter = db.traversal().V().has("class-name", className);

          while (iter.hasNext())
          {
             res.add(iter.next());
          }

          return res.iterator();
       }

       /**
        * 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 static Vertex findUniqueNode(String key, Object val)
       {
          Vertex res = null;

          Iterator<Vertex> iter = db.traversal().V().has(key, val).limit(2);

          while (iter.hasNext())
          {
             Vertex v = iter.next();

             if (res == null)
             {
                res = v;
             }
             else
             {
                final String msg = "More than one node found with key [%s] and value [%s]!";
                throw new RuntimeException(String.format(msg, key, val));
             }
          }

          return res;
       }

       /**
        * 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 static Vertex findNodeById(int nodeType, Object id)
       {
          Iterator<Vertex> iter = db.traversal().V().has("node-id", id)
                                                    .has("node-type", nodeType)
                                                    .limit(1);

          return iter.hasNext() ? iter.next() : null;
       }

       /**
        * 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 #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)
          {
             String folder = Configuration.getParameter("callgraph-db-folder",
                                                        DEFAULT_CALLGRAPH_DB_FOLDER);

             // handle the case where the current directory is different from the project home
             folder = Configuration.forceHome(folder);

             org.apache.commons.configuration2.Configuration config = new BaseConfiguration();
             config.setProperty("storage.directory", folder);
             config.setProperty("storage.backend", "berkeleyje");
             config.setProperty("index.search.backend", "lucene");
             config.setProperty("index.search.directory", folder + File.separator + "searchindex");

             // enable db-level cache
             config.setProperty("cache.db-cache", true);
             // do not pull all properties when reading a vertex
             config.setProperty("query.fast-property", false);

             // override properties
             File propFile = new File("cfg/callgraph.properties");
             if (propFile.exists())
             {
                Properties properties = new Properties();
                try
                {
                   properties.load(new FileReader(propFile));
                }
                catch (IOException e)
                {
                   LOG.log(Level.SEVERE, "", e);
                }

                for (String prop : properties.stringPropertyNames())
                {
                   config.setProperty(prop, properties.getProperty(prop));
                }
             }

             db = GraphDB.obtainGraphDB(config);
          }

          return db;
       }

       /**
        * Shutdown the graph DB.
        */
       public static void shutdownGraph()
       {
          if (db != null)
          {
             JanusGraph graph = (JanusGraph) db;

             graph.close();

             db = null;
          }
       }

       /**
        * 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 static String prepareFilename(String filename)
       {
          if (filename.trim().equals(""))
             return null;

          // TODO: if we have been provided an absolute path, these kind of files need to be marked in
          //  the graph, because an absolute path hard-coded in the 4GL code will not resolve well at
          // runtime.  all paths need to be relative to basepath

          // transform to unix-like separators
          if (isWindowsOs())
          {
             filename = filename.replaceAll("\\\\", "/");
          }

          if (!isCaseSensitive())
          {
             filename = filename.toLowerCase();
          }

          return filename;
       }

       /**
        * Obtain the vertices associated with the specified AST files.
        *
        * @param    files
        *           List of AST filenames 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.
        */
       public static List<Vertex> loadFileNodes(String[] files, boolean relative)
       {
          if (files == null)
          {
             return null;
          }

          List<Vertex> results = new ArrayList<>();

          for (int i = 0; i < files.length; i++)
          {
             String filename = files[i];

             if (!relative)
             {
                // convert to project-relative
                filename = Configuration.normalizeFilename(filename);
             }

             // discard the .ast suffix
             if (filename.endsWith(".ast"))
             {
                filename = filename.substring(0, filename.length() - ".ast".length());
             }
             else
             {
                LOG.log(Level.WARNING, "Non-AST entry point received:" + filename);
                continue;
             }

             // when using physical files, search by OS filename
             Iterator<Vertex> itrv = db.traversal().V().has("os-filename", filename).limit(2);

             Vertex v = null;

             if (itrv.hasNext())
             {
                v = itrv.next();

                if (itrv.hasNext())
                {
                   throw new RuntimeException("More than one file named " + filename + " found!");
                }
             }
             else
             {
                LOG.log(Level.WARNING, "Entry point not found:" + filename);
                continue;
             }

             results.add(v);
          }

          return results;
       }

       /**
        * Find the parent vertex having the given type.
        *
        * @param    node
        *           The node to find.
        * @param    type
        *           The parent's type.
        *
        * @return   See above.
        */
       public static Vertex findParentAstVertex(Vertex node, int type)
       {
          Vertex anode = node;
          if ((int) property(anode, "node-type") == type)
          {
             return anode;
          }

          String parentLabelName = parentProperty(type);
          Object parentId = property(anode, parentLabelName);
          if (parentId != null)
          {
             Vertex parent = db.traversal().V(parentId).limit(1).next();
             return parent;
          }

          int resDom = (int) property(anode, "resource-domain");
          if (resDom == EVENT)
          {
             // in this case, get its 'contains' edge
             anode = anode.edges(Direction.IN, "contains").next().outVertex();
             resDom = (int) property(anode, "resource-domain");
          }

          if (resDom != AST_NODE && resDom != FILE_RESOURCE)
          {
             throw new IllegalArgumentException("Can work only with AST vertices!");
          }

          Iterator<Vertex> iter = db.traversal()
                                    .V(anode)
                                    .inE("contains")
                                    .outV()
                                    .limit(2);
          if (iter.hasNext())
          {
             anode = iter.next();

             if (iter.hasNext())
             {
                throw new IllegalStateException("Multiple CONTAINS edges found!");
             }

             Vertex parent = findParentAstVertex(anode, type);

             if (parent != null)
             {
                // cache this parent ID
                node.property(parentLabelName, parent.id());
             }

             return parent;
          }

          return null;
       }

       /**
        * Find the procedure file associated with the given AST node.
        *
        * @param    node
        *           The node to find.
        *
        * @return   See above.
        */
       public static Vertex findProcedureFile(Vertex node)
       {
          return findParentAstVertex(node, PROCEDURE_FILE);
       }

       /**
        * Find the schema file associated with the given AST node.
        *
        * @param    node
        *           The node to find.
        *
        * @return   See above.
        */
       public static Vertex findSchemaFile(Vertex node)
       {
          return findParentAstVertex(node, SCHEMA_FILE);
       }

       /**
        * 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 static Object property(Element element, String key)
       {
          Property<?> prop = element.property(key);

          return prop.isPresent() ? prop.value() : null;
       }

       /**
        * 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 static void property(Element element, String key, Object value)
       {
          if (value instanceof String)
          {
             element.<String>property(key, (String)value);
          }
          else if (value instanceof Long)
          {
             element.<Long>property(key, (Long)value);
          }
          else if (value instanceof Integer)
          {
             element.<Integer>property(key, (Integer)value);
          }
          else if (value instanceof Boolean)
          {
             element.<Boolean>property(key, (Boolean)value);
          }
          else
          {
             throw new RuntimeException("Missing support for " + value.getClass());
          }
       }

       /**
        * Get the vertex {@code node-key} property value.
        *
        * @param    v
        *           The vertex.
        *
        * @return   The property value.
        */
       public static <T> T getNodeKeyValue(Vertex v)
       {
          return v.<T>property(v.<String>property("node-key").value()).value();
       }
    }