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