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