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