LegacyBuiltInClassSanitizer.java

/*
** Module   : LegacyBuiltInClassSanitizer.java
** Abstract : Tool to check and fix builtin legacy OO classes in p2j.oo package.
**
** Copyright (c) 2021-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------------Description----------------------------------------
** 001 CA  20210221 First version.  Injects missing 'qualified' and 'returns' annotations at method or 
**                  parameter.
** 002 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 org.reflections.*;

import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.util.*;

import javassist.*;
import javassist.bytecode.*;
import javassist.bytecode.SignatureAttribute.*;

/**
 * This tool allows an automated approach at sanitizing the {@link LegacySignature} annotations for 
 * hand-written legacy builtin classes, which exists in the <code>com.goldencode.p2j.oo</code> package.
 * <p>
 * To run this in test mode (which will just print the problems), use the <code>(t)est</code> mode.
 * <p>
 * The following {@link LegacySignature} attributes are verified and injected (if possible):
 * <ul>
 *    <li>'qualified', for {@link object} methods or parameters</li>
 *    <li>'extent', for extent methods or parameters - these are not injected, as the Java signature for the 
 *         method doesn't allow encoding of the extent type (fixed or dynamic).</li>
 *    <li>'returns', for method return type.
 * </ul>
 * <p>
 * If the parameter count at {@link LegacySignature} doesn't match the paramter count at the Java method
 * definition, a message will be logged.
 * 
 * TODO: sanitize full Java method signature against the {@link LegacySignature}.
 */
public class LegacyBuiltInClassSanitizer
{
   /** Flag indicating if we are in test mode - no changes will be made to source code. */
   private static boolean testMode = false;
   
   /**
    * Inject the <code>returns</code> attribute (with the method's legacy return type) at the 
    * {@link LegacySignature} annotation.
    * 
    * @param    cc
    *           The target class.
    * @param    m
    *           The target method.
    * @param    clsLines
    *           The source code.
    *           
    * @return   The read source code.
    */
   private static List<String> injectMethodReturn(CtClass cc, CtMethod m, List<String> clsLines) 
   throws Exception
   {
      if (clsLines == null)
      {
         clsLines = readSourceCode(cc);
      }
      
      CodeAttribute code = (CodeAttribute) m.getMethodInfo().getAttribute(CodeAttribute.tag);
      int idx = code == null ? findLegacySignatureLine(cc, m, clsLines) 
                             : findLegacySignatureLine(code, clsLines);
      if (idx == -1)
      {
         // not found
         return clsLines;
      }
      
      CtClass mret = m.getReturnType();
      if (mret.getName().equals(Object.class.getName()))
      {
         return clsLines;
      }
      
      String returns = mret.getSimpleName().toUpperCase();
      if (returns.indexOf('<') > 0)
      {
         returns = returns.substring(0, returns.indexOf('<'));
      }
      if (returns.indexOf('[') > 0)
      {
         returns = returns.substring(0, returns.indexOf('['));
      }
      returns = returns.trim();
      
      String pline = clsLines.get(idx);
      for (int j = 0; j < pline.length(); j++)
      {
         if (pline.charAt(j) == '(')
         {
            // inject the 'qualified' annotation
            String s1 = pline.substring(0, j + 1);
            String s2 = pline.substring(j + 1);
            pline = s1 + "returns = \"" + returns + "\", " + s2;
            clsLines.set(idx, pline);
            break;
         }
      }
      
      return clsLines;
   }

   /**
    * Inject the <code>qualified</code> attribute (with the method's legacy qualified type) at the 
    * {@link LegacySignature} annotation.  Only if the Java method returns {@link object}.
    * 
    * @param    cc
    *           The target class.
    * @param    m
    *           The target method.
    * @param    clsLines
    *           The source code.
    *           
    * @return   The read source code.
    */
   private static List<String> injectQualifiedReturn(CtClass cc, CtMethod m, List<String> clsLines) 
   throws Exception
   {
      if (clsLines == null)
      {
         clsLines = readSourceCode(cc);
      }
      
      CodeAttribute code = (CodeAttribute) m.getMethodInfo().getAttribute(CodeAttribute.tag);
      int idx = code == null ? findLegacySignatureLine(cc, m, clsLines) 
                             : findLegacySignatureLine(code, clsLines);
      if (idx == -1)
      {
         // not found
         return clsLines;
      }
      
      MethodSignature sig = SignatureAttribute.toMethodSignature(m.getSignature());
      String qualified = getLegacyType(sig, (ClassType) sig.getReturnType());

      String pline = clsLines.get(idx);
      for (int j = 0; j < pline.length(); j++)
      {
         if (pline.charAt(j) == '(')
         {
            // inject the 'qualified' annotation
            String s1 = pline.substring(0, j + 1);
            String s2 = pline.substring(j + 1);
            pline = s1 + "qualified = \"" + qualified + "\", " + s2;
            clsLines.set(idx, pline);
            break;
         }
      }
      
      return clsLines;
   }
   
   /**
    * Inject the <code>qualified</code> attribute (with the parameter's legacy qualified type) at the 
    * {@link LegacyParameter} annotation.  Only if the Java parameter's type is {@link object}.
    * 
    * @param    cc
    *           The target class.
    * @param    m
    *           The target method.
    * @param    pidx
    *           The parameter index.
    * @param    clsLines
    *           The source code.
    *           
    * @return   The read source code.
    */
   private static List<String> injectQualifiedParameter(CtClass      cc, 
                                                        CtMethod     m,
                                                        int          pidx,
                                                        List<String> clsLines) 
   throws Exception 
   {
      if (clsLines == null)
      {
         clsLines = readSourceCode(cc);
      }
      
      CodeAttribute code = (CodeAttribute) m.getMethodInfo().getAttribute(CodeAttribute.tag);
      int idx = code == null ? findLegacySignatureLine(cc, m, clsLines) 
                             : findLegacySignatureLine(code, clsLines);
      
      if (idx == -1)
      {
         // not found
         return clsLines;
      }
      
      // find the parameter
      int i = -1;
      int pLineIdx = -1;
      String lpAnno = "@" + LegacyParameter.class.getSimpleName();
      l1: while (true)
      {
         String line = clsLines.get(idx);
         pLineIdx = line.indexOf(lpAnno);
         while (pLineIdx >= 0)
         {
            i = i + 1;

            if (i == pidx)
            {
               break l1;
            }

            pLineIdx = line.indexOf(lpAnno, pLineIdx + lpAnno.length());
         }
         idx = idx + 1;
      }
      
      MethodSignature sig = SignatureAttribute.toMethodSignature(m.getSignature());
      String qualified = getLegacyType(sig, (ClassType) sig.getParameterTypes()[pidx]);
      
      String pline = clsLines.get(idx);
      for (int j = pLineIdx; j < pline.length(); j++)
      {
         if (pline.charAt(j) == '(')
         {
            // inject the 'qualified' annotation
            String s1 = pline.substring(0, j + 1);
            String s2 = pline.substring(j + 1);
            pline = s1 + "qualified = \"" + qualified + "\", " + s2;
            clsLines.set(idx, pline);
            break;
         }
      }
      
      return clsLines;
   }
   
   /**
    * Find the line which holds the {@link LegacySignature} annotation for the given method.
    * <p>
    * This uses a heuristic to find the Java source code for the specified method.
    *  
    * @param    cc
    *           The target class.
    * @param    m
    *           The target method.
    * @param    clsLines
    *           The source code.
    *           
    * @return   The found line, or <code>-1</code> if can't be resolved.
    */
   private static int findLegacySignatureLine(CtClass cc, CtMethod m, List<String> clsLines) 
   throws Exception
   {
      int idx = -1;
      
      for (int i = 0; i < clsLines.size(); i++)
      {
         String line = clsLines.get(i);
         boolean hadAnno = false;
         while (line.trim().startsWith("@"))
         {
            // find the line where the method definition begins and build its signature.
            int count = countParenthesis(line);
            int j = i + 1;
            while (count != 0)
            {
               count += countParenthesis(clsLines.get(j));
               j = j + 1;
            }
            
            i = j;
            line = clsLines.get(i);
            hadAnno = true;
         }
         
         if (hadAnno)
         {
            // read the method definition
            String sig = line;
            int count = countParenthesis(line);
            int j = i + 1;
            while (count != 0)
            {
               count += countParenthesis(clsLines.get(j));
               sig += clsLines.get(j);
               j = j + 1;
            }
            
            if (sig.indexOf('(') > 0)
            {
               String name = sig.substring(0, sig.indexOf('(')).trim();
               name = name.substring(name.lastIndexOf(' ')).trim();
               
               if (m.getName().equals(name) && parameterMatch(m, sig))
               {
                  return findLegacySignatureLine(i, clsLines);
               }
            }
         }
      }
      
      return idx;
   }
   
   /**
    * Check if the given Java-style signature matches the specified method.
    * 
    * @param    m
    *           The target method.
    * @param    sig
    *           The candidate signature.
    *           
    * @return   <code>true</code> if the method has the same signature.
    */
   private static boolean parameterMatch(CtMethod m, String sig) 
   throws NotFoundException
   {
      String spars = sig.substring(sig.indexOf('(') + 1);
      spars = spars.substring(0, spars.indexOf(')'));
      spars = spars.trim();
      if (spars.isEmpty())
      {
         return m.getParameterTypes().length == 0;
      }
      String[] pars = spars.split(",", -1);
      if (pars.length != m.getParameterTypes().length)
      {
         return false;
      }
      
      for (int i = 0; i < pars.length; i++)
      {
         String par = pars[i].trim();
         
         par = par.substring(0, par.lastIndexOf(' '));
         par = par.trim();
         boolean isArray = par.endsWith("[]");

         if (par.indexOf('<') > 0)
         {
            par = par.substring(0, par.indexOf('<'));
         }
         par = par.trim();
         if (par.indexOf(' ') > 0) 
         {
            par = par.substring(par.lastIndexOf(' ') + 1);
            par = par.trim();
         }
         if (isArray && !par.endsWith("]"))
         {
            par += "[]";
         }
         
         CtClass ptype = m.getParameterTypes()[i];
         if (!ptype.getSimpleName().equals(par))
         {
            return false;
         }
      }
      
      return true;
   }
   
   /**
    * Count the number of opened parenthesis.
    * 
    * @param    line
    *           The line of code.
    *           
    * @return   The number of opened (or closed) parenthesis.
    */
   private static int countParenthesis(String line)
   {
      int n = 0;
      for (int i = 0; i < line.length(); i++)
      {
         char c = line.charAt(i);
         n += (c == ')' ? -1 : c == '(' ? 1 : 0);
      }
      
      return n;
   }
   
   /**
    * Find the line which holds the {@link LegacySignature} annotation for the given method.
    * <p>
    * This uses the disassembled bytecode to position on the method code, and walk 'up' until the 
    * {@link LegacySignature} is found.
    *  
    * @param    code
    *           The method's code attribute.
    * @param    clsLines
    *           The source code.
    *           
    * @return   The found line, or <code>-1</code> if can't be resolved.
    */
   private static int findLegacySignatureLine(CodeAttribute code, List<String> clsLines)
   {
      LineNumberAttribute lna = (LineNumberAttribute) code.getAttribute(LineNumberAttribute.tag);
      int firstCodeLine = lna.lineNumber(0);
      return findLegacySignatureLine(firstCodeLine, clsLines);
   }
   
   /**
    * Find the line which holds the {@link LegacySignature} annotation for the given method, by walking 'up' 
    * until the {@link LegacySignature} is found.
    *  
    * @param    firstCodeLine
    *           The method's first code line.
    * @param    clsLines
    *           The source code.
    *           
    * @return   The found line, or <code>-1</code> if can't be resolved.
    */
   private static int findLegacySignatureLine(int firstCodeLine, List<String> clsLines)
   {
      String lsAnno = "@" + LegacySignature.class.getSimpleName();
      
      int idx = firstCodeLine;
      // find the LegacySignature line
      while (true)
      {
         String line = clsLines.get(idx);
         if (line.indexOf(lsAnno) > 0)
         {
            break;
         }
         idx = idx - 1;
      }
      
      return idx;
   }
   
   /**
    * Find the legacy OO type.
    * 
    * @param    sig
    *           The method signature. 
    * @param    tp
    *           The type to resolve.
    *           
    * @return   The found legacy type.
    */
   private static String getLegacyType(MethodSignature sig, ClassType tp) 
   throws ClassNotFoundException
   {
      TypeArgument ta = tp.getTypeArguments()[0];
      String jvmType = ta.getType().toString();
      if (jvmType.length() == 1)
      {
         // generic type defined at method
         for (TypeParameter tparam : sig.getTypeParameters())
         {
            if (tparam.getName().equals(jvmType))
            {
               jvmType = tparam.getClassBound().toString();
            }
         }
         
      }
      
      Class<?> argCls = Class.forName(jvmType);
      
      String qualified = argCls == _BaseObject_.class 
                           ? "Progress.Lang.Object" 
                           : ((LegacyResource) argCls.getAnnotation(LegacyResource.class)).resource();
      
      return qualified;
   }
   
   /**
    * Read the Java source code for the given class.
    * 
    * @param    cc
    *           The class to be read.
    *           
    * @return   The source code.
    */
   private static List<String> readSourceCode(CtClass cc) 
   {
      try
      {
         String pkg = cc.getPackageName();
         pkg = pkg.replace('.', File.separatorChar);
         String clsFile = "src" + File.separator + pkg + File.separator + cc.getClassFile().getSourceFile();
         BufferedReader br = new BufferedReader(new FileReader(new File(clsFile)));
         
         List<String> lines = new ArrayList<>();
         String line;
         while ((line = br.readLine()) != null)
         {
            lines.add(line);
         }
         br.close();
         
         return lines;
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }
   
   /**
    * Write the santizied Java source code for the given class.
    * 
    * @param    cc
    *           The class to be written.
    * @param    clsLines
    *           The source code.
    */
   private static void writeSourceCode(CtClass cc, List<String> clsLines)
   {
      try
      {
         String pkg = cc.getPackageName();
         pkg = pkg.replace('.', File.separatorChar);
         String clsFile = "src" + File.separator + pkg + File.separator + cc.getClassFile().getSourceFile();
         BufferedWriter lnr = new BufferedWriter(new FileWriter(new File(clsFile)));
         
         for (String line : clsLines)
         {
            lnr.write(line);
            lnr.write(System.lineSeparator());
         }
         lnr.close();
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }

   /**
    * Run the tool.  Use <code>(t)est</code> to run in 'test mode', with no side-effects for existing code - 
    * this will only report any found problems in the <code>com.goldencode.p2j.oo</code> package.
    * 
    * @param    args
    *           The arguments.
    */
   public static void main(String[] args)
   throws Exception
   {
      System.out.println("WARNING! This tool will overwrite source files in com.goldencode.p2j.oo package!");
      System.out.println("WARNING! Save any changes before running this tool!");
      System.out.println("WARNING! Recompile the FWD code before running the tool again.");
      System.out.println("Use (t)est to run this tool in test mode (no changes will be made).");
      
      Console console = System.console();
      BufferedReader reader = console == null ? new BufferedReader(new InputStreamReader(System.in)) 
                                              : new BufferedReader(console.reader());
      try
      {
         String line;
         do
         {
            System.out.print("Continue [ (y)es/(n)o/(t)est ]? ");
            
            line = reader.readLine();
            if ("y".equals(line))
            {
               break;
            }
            else if ("t".equals(line))
            {
               testMode = true;
               break;
            }
            else if ("n".equals(line))
            {
               return;
            }
         }
         while (line != null);
      }
      finally
      {
         reader.close();
      }
      
      ClassPool pool = ClassPool.getDefault();

      Reflections reflections = new Reflections("com.goldencode.p2j.oo");
      Set<Class<? extends _BaseObject_>> legacyClasses = reflections.getSubTypesOf(_BaseObject_.class);
      
      for (Class<? extends _BaseObject_> cls : legacyClasses) 
      {
         CtClass cc = pool.get(cls.getName());
         List<String> clsLines = null;
         
         for (CtMethod m : cc.getDeclaredMethods())
         {
            LegacySignature ls = (LegacySignature) m.getAnnotation(LegacySignature.class);
            if (ls == null)
            {
               continue;
            }

            CtClass[] ptypes = m.getParameterTypes();
         
            if (ls.parameters().length != ptypes.length)
            {
               System.out.println("Parameter count and signature parameters count do not match: " + 
                                  m.getLongName());
               continue;
            }
            
            // inject 'qualified' annotation at 'object' parameters
            for (int i = 0; i < ptypes.length; i++)
            {
               LegacyParameter lp = ls.parameters()[i];
               if (ptypes[i].getName().equals(object.class.getName()) && lp.qualified().isEmpty())
               {
                  System.out.println("'object' parameter " + i + " has no 'qualified' annotation for method: " + 
                                     m.getLongName());
                  clsLines = injectQualifiedParameter(cc, m, i, clsLines);
               }

               // inject 'extent' annotation at extent parameters
               if ((ptypes[i].isArray()                                                           || 
                    ptypes[i].getSimpleName().equals(OutputExtentParameter.class.getSimpleName()) ||
                    ptypes[i].getSimpleName().equals(InputOutputExtentParameter.class.getSimpleName())) && 
                   lp.extent() == SourceNameMapper.NO_EXTENT)
               {
                  System.out.println("Parameter " + i + " is extent but has no 'extent' annotation for method: " + 
                                     m.getLongName());
               }
            }
            
            // inject 'qualified' annotation at methods returning 'object'
            if (m.getReturnType().getName().equals(object.class.getName()) && ls.qualified().isEmpty())
            {
               System.out.println("Return type is 'object' but has no 'qualified' annotation at its signature: " +
                                  m.getLongName());
               clsLines = injectQualifiedReturn(cc, m, clsLines);
            }
            
            // inject 'returns' annotation at methods returning non-void
            if (m.getReturnType() != CtClass.voidType && 
                (ls.returns().equalsIgnoreCase("void") || ls.returns().isEmpty()))
            {
               System.out.println("Method return type is " + m.getReturnType().getSimpleName() + 
                                  " but has no 'returns' annotation: " + m.getLongName());
               clsLines = injectMethodReturn(cc, m, clsLines);
            }

            // inject 'extent' annotation at methods returning extent
            if (m.getReturnType().isArray() && ls.extent() == SourceNameMapper.NO_EXTENT)
            {
               System.out.println("Method return type is " + m.getReturnType().getSimpleName() + 
                                  " but has no 'extent' annotation: " + m.getLongName());
            }
         }
         
         if (!testMode && clsLines != null)
         {
            writeSourceCode(cc, clsLines);
         }
      }
   }
}