blob: 5927e034da7d455c2f8202d87174593920392a7b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.uima.migratev3.jcas;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import org.apache.uima.UIMARuntimeException;
import org.apache.uima.cas.impl.TypeImpl;
import org.apache.uima.cas.impl.TypeSystemImpl;
import org.apache.uima.cas.impl.UimaDecompiler;
import org.apache.uima.internal.util.CommandLineParser;
import org.apache.uima.internal.util.Misc;
import org.apache.uima.internal.util.UIMAClassLoader;
import org.apache.uima.pear.tools.PackageBrowser;
import org.apache.uima.pear.tools.PackageInstaller;
import org.apache.uima.util.FileUtils;
import com.github.javaparser.ASTHelper;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseException;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.PackageDeclaration;
import com.github.javaparser.ast.body.AnnotationDeclaration;
import com.github.javaparser.ast.body.BodyDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.EmptyTypeDeclaration;
import com.github.javaparser.ast.body.EnumDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.body.VariableDeclaratorId;
import com.github.javaparser.ast.expr.AssignExpr;
import com.github.javaparser.ast.expr.BinaryExpr;
import com.github.javaparser.ast.expr.CastExpr;
import com.github.javaparser.ast.expr.EnclosedExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.FieldAccessExpr;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.ast.expr.NullLiteralExpr;
import com.github.javaparser.ast.expr.ObjectCreationExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.stmt.BlockStmt;
import com.github.javaparser.ast.stmt.EmptyStmt;
import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt;
import com.github.javaparser.ast.stmt.ExpressionStmt;
import com.github.javaparser.ast.stmt.IfStmt;
import com.github.javaparser.ast.stmt.ReturnStmt;
import com.github.javaparser.ast.stmt.Statement;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.PrimitiveType;
import com.github.javaparser.ast.type.ReferenceType;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
/**
* A driver that scans given roots for source and/or class Java files that contain JCas classes
*
* - identifies which ones appear to be JCas classes (heuristic)
* -- identifies which ones appear to be v2
* --- converts these to v3
*
* - also can receive a list of individual class names
*
* Creates summary and detailed reports of its actions.
*
* Outputs converted files to an output file tree.
*
* - includes reporting on multiple definitions of the same class
*
* Directory structure, starting at -outputDirectory
* converted/
* v2/
* x/y/z/javapath/.../Classname.java
* x/y/z/javapath/.../Classname.java
* ...
* v3/
* x/y/z/javapath/.../Classname.java
* x/y/z/javapath/.../Classname.java
* ...
* 1/ << for duplicates, each set is for identical dups, different sets for non-identical
* x/y/z/javapath/.../Classname.java << for duplicates, each set is for identical dups, different sets for non-identical
* x/y/z/javapath/.../Classname.java << for duplicates, each set is for identical dups, different sets for non-identical
* ...
* 2/ << for duplicates, each set is for identical dups, different sets for non-identical
* x/y/z/javapath/.../Classname.java << for duplicates, each set is for identical dups, different sets for non-identical
* x/y/z/javapath/.../Classname.java << for duplicates, each set is for identical dups, different sets for non-identical
* ...
*
* not-converted/
* logs/
* processed.txt
* builtinsNotExtended.txt
* ...
*
* Operates in one of two modes:
* Mode 1: Given classes-roots and/or individual class names, and a classpath,
* scans the classes-routes looking for classes candidates, or iterate through the individual class names
* - determines the class name,
* - looks up the right "version" in the provided classpath, and decompiles that
* - migrates that decompiled source.
* -- duplicates are also processed. If different they are put into v3/nnn/etc.
*
* Mode 2: Given sources-roots
* Duplicates are migrated, results are put into a v3/nnn/ rest-of-path-identical
*
* Note: Each run clears the output directory before starting the migration.
*/
public class MigrateJCas extends VoidVisitorAdapter<Object> {
private static final String SOURCE_FILE_ROOTS = "-sourcesRoots";
private static final String CLASS_FILE_ROOTS = "-classesRoots";
private static final String OUTPUT_DIRECTORY = "-outputDirectory";
private static final String SKIP_TYPE_CHECK = "-skipTypeCheck";
private static final String MIGRATE_CLASSPATH = "-migrateClasspath";
private static final String CLASSES = "-classes"; // individual classes to migrate, get from supplied classpath
/*****************
* Candidate
*****************/
private static final class Candidate {
/**
* path to the .class or .java file
*/
final Path p;
/**
* null or (if in a Pear), the pear's classpath (from installing and then getting the classpath)
* - includes jars in lib/ dir and the bin/ classes
*/
String pearClasspath;
Candidate(Path p) {
this.p = p;
}
Candidate(Path p, String pearClasspath) {
this.p = p;
this.pearClasspath = pearClasspath;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
String pear = (pearClasspath == null) ? "" :
", pearClasspath=" + pearClasspath;
return "Candidate [p=" + p + pear + "]";
}
}
/*****************
* P E A R or J A R
*
* Information for each PEAR or JAR that is processed
* Used when post-processing pears
*****************/
private static final class PearOrJar {
/**
* path to original .pear or .jar file among the roots
*/
final Path pathToPearOrJar;
/**
* path to .class file in pear e.g. bin/org/apache/uima/examples/tutorial/Sentence.class
* or in jar e.g. org/apache/uima/examples/tutorial/Sentence.class
*/
final List<String> pathsToCandidateFiles = new ArrayList<>();
PearOrJar(Path pathToPearOrJar) {
this.pathToPearOrJar = pathToPearOrJar;
}
}
/**
* Map
* key
*/
final List<String> classnames = new ArrayList<>();
private Path tempDir = null;
private boolean isSource = false;
private Candidate candidate;
private List<Candidate> candidates;
private PearOrJar pear_current;
private Deque<PearOrJar> jar_current_stack = new ArrayDeque<>();
private List<PearOrJar> pears = new ArrayList<>();
private List<PearOrJar> jars = new ArrayList<>();
/**
* current Pear install path + 1 more dir in temp dir
* used in candidate generation to relativize the path to just the part inside the pear
*/
private Path pearResolveStart;
/**
* Map created when adding a pear's .class/.source file to candidates
* Key: path string to .class or .java file in an installed Pear
* Value: path part corresponding to inside pear - delete install dir + 1 more dir from front
*/
private Map<String, String> path2InsidePearOrJarPath = new HashMap<>();
/**
* Map created when adding a pear's .class/.source file to candidates
* Key: path string to .class or .java file in an installed Pear
* Value: path part corresponding to just the classname
*/
private Map<String, String> path2classname = new HashMap<>();
private String packageName;
private String className; // (omitting package)
private String packageAndClassNameSlash;
private final Set<String> usedPackageAndClassNames = new HashSet<>();
private int packageAndClassNameSlash_i;
/** includes trailing / */
private String outputDirectory;
/** includes trailing / */
private String outDirConverted;
/** includes trailing / */
private String outDirSkipped;
/** includes trailing / */
private String outDirLog;
private String[] sourcesRoots = new String[0];
private String[] classesRoots = new String[0];
private CompilationUnit cu;
// save this value in the class instance to avoid recomputing it
private ClassLoader cachedMigrateClassLoader = null;
private String migrateClasspath = null;
private String individualClasses = null; // to decompile
private class ConvertedSource {
String rOrigSource; // remembered original source
byte[] rOrigBytes; // remembered original bytes
List<Path> paths;
ConvertedSource(String origSource, byte[] origBytes, Path path) {
this.rOrigSource = origSource;
this.rOrigBytes = origBytes;
paths = new ArrayList<>();
add(path);
}
void add(Path path) {
paths.add(path);
}
}
/**
* A map from classnames (fully qualified, with slashes) to a list of converted sources
* one per non-duplicated source
*/
private Map<String, List<ConvertedSource>> classname2multiSources = new TreeMap<>();
private Map<byte[], String> origBytesToClassName = new HashMap<>();
private Map<String, String> origSourceToClassName = new HashMap<>();
/************************************
* Reporting
************************************/
private final List<Path> v2JCasFiles = new ArrayList<>();
private final List<Path> v3JCasFiles = new ArrayList<>();
private final List<PathAndReason> nonJCasFiles = new ArrayList<>(); // path, reason
private final List<PathAndReason> failedMigration = new ArrayList<>(); // path, reason
private final List<ClassnameAndPath> c2ps = new ArrayList<>(); // class, path
private final List<ClassnameAndPath> skippedBuiltins = new ArrayList<>(); // class, path
private final List<PathAndReason> deletedCheckModified = new ArrayList<>(); // path, deleted check string
private final List<String1AndString2> pathWorkaround = new ArrayList<>(); // original, workaround
private final List<String1AndString2> pearClassReplace = new ArrayList<>(); // pear, classname
private final List<String1AndString2> jarClassReplace = new ArrayList<>(); // jar, classname
private final List<PathAndReason> manualInspection = new ArrayList<>(); // path, reason
// private final List<PathAndPath> embeddedJars = new ArrayList<>(); // source, temp
private boolean v2; // false at start of migrate, set to true if a v2 class candidate is discovered
private boolean v3; // true at start of migrate, set to false if no conversion done
/************************************
* Context for visits
************************************/
/**
* if non-null, we're inside the ast for a likely JCas getter or setter method
*/
private MethodDeclaration get_set_method;
private String featName;
private boolean isGetter;
private boolean isArraySetter;
/**
* the range name part for _getXXXValue.. calls
*/
private Object rangeNamePart;
/**
* the range name part for _getXXXValue.. calls without converting Ref to Feature
*/
private String rangeNameV2Part;
/**
* temp place to insert static final int feature declarations
*/
private List<BodyDeclaration> fi_fields = new ArrayList<>();
private Set<String> featNames = new HashSet<>();
private boolean hasV2Constructors;
private boolean hasV3Constructors;
private boolean isSkipTypeCheck = false;
private byte[] origBytes; // set by getSource()
private String origSource; // set by getSource()
private String alreadyDone; // the slashifiedClassName
private boolean isOk;
public MigrateJCas() {
}
public static void main(String[] args) {
(new MigrateJCas()).run(args);
}
/***********************************
* Main
* @param args -
***********************************/
void run(String[] args) {
CommandLineParser clp = parseCommandArgs(args);
// clear output dir
FileUtils.deleteRecursive(new File(outputDirectory));
isSource = true;
processRootsCollection("source", sourcesRoots, clp);
isSource = false;
processRootsCollection("classes", classesRoots, clp);
if (individualClasses != null) {
processCollection("individual classes: ", new Iterator<String>() {
Iterator<String> it = Arrays.asList(individualClasses.split(File.pathSeparator)).iterator();
public boolean hasNext() {return it.hasNext();}
public String next() {
return prepareIndividual(it.next());}
});
}
isOk = report();
postProcessing();
System.out.println("Migration finished " +
(isOk
? "with no unusual conditions."
: "with 1 or more unusual conditions that need manual checking."));
}
private void postProcessing() {
if (javaCompilerAvailable()) {
compileV3sources();
postProcessPearsOrJars("jars" , jars , jarClassReplace);
postProcessPearsOrJars("pears", pears, pearClassReplace);
//
// try {
// Path pearOutDir = Paths.get(outputDirectory, "pears");
// FileUtils.deleteRecursive(pearOutDir.toFile());
// Files.createDirectories(pearOutDir);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
//
// System.out.format("replacing .class files in %,d PEARs%n", pears.size());
// for (PearOrJar p : pears) {
// pearOrJarPostProcessing(p);
// }
// try {
// reportPaths("Reports of updated Pears", "pearFileUpdates.txt", pearClassReplace);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
}
}
private void postProcessPearsOrJars(String kind, List<PearOrJar> pearsOrJars, List<String1AndString2> classReplace) { // pears or jars
try {
Path outDir = Paths.get(outputDirectory, kind);
FileUtils.deleteRecursive(outDir.toFile());
Files.createDirectories(outDir);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (pearsOrJars.size() == 0) {
System.out.format("No .class files were replaced in %s%n.", kind);
} else {
System.out.format("replacing .class files in %,d %s%n", pearsOrJars.size(), kind);
for (PearOrJar p : pearsOrJars) {
pearOrJarPostProcessing(p, kind);
}
try {
reportPaths("Reports of updated " + kind, kind + "FileUpdates.txt", classReplace);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void pearOrJarPostProcessing(PearOrJar pearOrJar, String kind) { // pears or jars
try {
final boolean isPear = kind.equals("pears");
// copy the pear so we don't change the original
Path pearOrJarCopy = Paths.get(outputDirectory, kind, pearOrJar.pathToPearOrJar.getFileName().toString());
Files.copy(pearOrJar.pathToPearOrJar, pearOrJarCopy);
// put up a file system on the pear or jar
FileSystem pfs = FileSystems.newFileSystem(pearOrJarCopy, null);
// replace the .class files in this pear with corresponding v3 ones
for (int i = 0; i < pearOrJar.pathsToCandidateFiles.size(); i++) {
String candidatePath = pearOrJar.pathsToCandidateFiles.get(i);
String path_in_v3_classes = isPear
? getPath_in_v3_classes(candidatePath)
: candidatePath;
Path src = Paths.get(outputDirectory, "converted/v3-classes", path_in_v3_classes
+ (isPear ? ".class" : ""));
Path tgt = pfs.getPath(
"/",
isPear
? path2InsidePearOrJarPath.get(candidatePath) // needs to be bin/org/... etc
: candidatePath); // needs to be org/... etc
if (Files.exists(src)) {
Files.copy(src, tgt, StandardCopyOption.REPLACE_EXISTING);
reportPearOrJarClassReplace(pearOrJarCopy.toString(), path_in_v3_classes, kind);
}
}
pfs.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @param pathInPear a complete path to a class inside an (installed) pear
* @return the part starting after the top node of the install dir
*/
private String getPath_in_v3_classes(String pathInPear) {
return path2classname.get(pathInPear);
}
/**
* When running the compiler to compile v3 sources, we need a classpath that at a minimum
* includes uimaj-core. The strategy is to use the invoker of this tool's classpath as
* specified from the application class loader
*/
private void compileV3sources() {
if (c2ps.size() == 0) {
return;
}
System.out.format("Compiling converted %,d classes%n", c2ps.size());
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, Charset.forName("UTF-8"));
Iterable<String> compilationUnitStrings = new Iterable<String>() {
@Override
public Iterator<String> iterator() {
return new Iterator<String>() {
int pos = 0;
@Override
public boolean hasNext() {
return pos < c2ps.size();
}
@Override
public String next() {
ClassnameAndPath classnameAndPath = c2ps.get(pos++);
return outDirConverted + "v3/" + classnameAndPath.classname + ".java";
}
};
}
};
Iterable<? extends JavaFileObject> compilationUnits =
fileManager.getJavaFileObjectsFromStrings(compilationUnitStrings);
// specify where the output classes go
String classesBaseDir = outDirConverted + "v3-classes";
try {
Files.createDirectories(Paths.get(classesBaseDir));
} catch (IOException e) {
throw new UIMARuntimeException(e);
}
// specify the classpath
String classpath = getAppClassPath();
Iterable<String> options = Arrays.asList("-d", classesBaseDir,
"-classpath", classpath);
compiler.getTask(null, fileManager, null, options, null, compilationUnits).call();
}
/**
* The classpath used to compile is
* - the classpath for this migration app except the target/classes for this project
* - any passed in migrate classpath
* @return the classpath to use in compiling the jcasgen'd sources
*/
private String getAppClassPath() {
URL[] urls = ((URLClassLoader)MigrateJCas.class.getClassLoader()).getURLs();
StringBuilder cp = new StringBuilder();
for (URL url : urls) {
String p = url.getFile().toString();
if (!p.endsWith("uimaj-v3migration-jcas/target/classes/")) {
cp.append(url.getFile().toString());
cp.append(File.pathSeparatorChar);
}
}
if (null != migrateClasspath) {
cp.append(migrateClasspath);
}
// System.out.println("debug: classpath = " + cp.toString());
return cp.toString();
}
private boolean javaCompilerAvailable() {
if (null == ToolProvider.getSystemJavaCompiler()) {
System.out.println("The migration tool would like to compile the migrated files, \n"
+ " but no Java compiler is available.\n"
+ " To make one available, run this tool using a Java JDK, not JRE");
return false;
}
return true;
}
private void processRootsCollection(String kind, String[] roots, CommandLineParser clp) {
for (String root : roots) {
processCollection("from " + kind + " root: " + root, new Iterator<String>() {
Iterator<Candidate> it = getCandidates(clp, root).iterator();
public boolean hasNext() {return it.hasNext();}
public String next() {
return prepare(it.next());}
});
}
}
private void processCollection(String sourceName, Iterator<String> sourceIterator) {
System.out.println("Migrating " + sourceName + ", number of classes migrated:");
int i = 1;
System.out.print(" ");
while (sourceIterator.hasNext()) {
migrate(sourceIterator.next());
if ((i % 50) == 0) System.out.format("%4d%s", Integer.valueOf(i), "\r");
i++;
}
System.out.format("%4d%n", Integer.valueOf(i - 1));
}
/**
* parse command line args
* @param args -
* @return the CommandLineParser instance
*/
private CommandLineParser parseCommandArgs(String[] args) {
CommandLineParser clp = createCmdLineParser();
try {
clp.parseCmdLine(args);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (!checkCmdLineSyntax(clp)) {
printUsage();
System.exit(2);
}
if (clp.isInArgsList(CLASS_FILE_ROOTS)) {
classesRoots = getRoots(clp, CLASS_FILE_ROOTS);
}
if (clp.isInArgsList(SOURCE_FILE_ROOTS)) {
sourcesRoots = getRoots(clp, SOURCE_FILE_ROOTS);
}
return clp;
}
private String[] getRoots(CommandLineParser clp, String kind) {
return clp.getParamArgument(kind).split("\\" + File.pathSeparator);
}
/**
* If working with .class files:
* - read the byte array
* - see if it has already been decompiled; if so, return false
*
* Side effect, set origSource and origBytes and alreadyDone
* @param c
* @return the source to maybe migrate
*/
private String getSource(Candidate c) {
try {
origSource = null;
origBytes = null;
if (isSource) {
origSource = FileUtils.reader2String(Files.newBufferedReader(c.p));
alreadyDone = origSourceToClassName.get(origSource);
// System.out.println("debug read " + s.length());
} else {
origBytes = Files.readAllBytes(c.p);
alreadyDone = origBytesToClassName.get(origBytes);
if (null == alreadyDone) {
origSource = decompile(origBytes, c.pearClasspath);
}
}
if (alreadyDone != null) {
return null;
}
return origSource;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Migrate one JCas definition.
*
* The source is either direct, or a decompiled version of a .class file (missing comments, etc.).
*
* This method only called if heuristics indicate this is a V2 JCas class definition.
*
* The goal is to preserve as much as possible of existing customization.
* The general approach is to parse the source into an AST, and use visitor methods.
* For getter/setter methods that are for features (heurstic), set up a context for inner visitors
* identifying the getter / setter.
* - reuse method declarator, return value casts, value expressions
* - remove feature checking statement, array bounds checking statement, if present.
* - replace the simpleCore (see Jg), replace the arrayCore
*
* For constructors, replace the 2-arg one that has arguments:
* addr and TOP_Type with the v3 one using TypeImpl, CasImpl.
*
* Add needed imports.
* Add for each feature the _FI_xxx static field declarator.
*
* Leave other top level things alone
* - additional constructors.
* - other methods not using jcasType refs
*
* @param source - the source, either directly from a .java file, or a decompiled .class file
*/
private void migrate(String source) {
if (alreadyDone == null) {
v3 = true; // preinit
v2 = false;
featNames.clear();
fi_fields.clear();
// System.out.println("Migrating source before migration:\n");
// System.out.println(source);
// System.out.println("\n\n\n");
StringReader sr = new StringReader(source);
try {
cu = JavaParser.parse(sr, true);
addImport("org.apache.uima.cas.impl.CASImpl");
addImport("org.apache.uima.cas.impl.TypeImpl");
addImport("org.apache.uima.cas.impl.TypeSystemImpl");
this.visit(cu, null);
new removeEmptyStmts().visit(cu, null);
if (v3) {
removeImport("org.apache.uima.jcas.cas.TOP_Type");
}
if (v3 && fi_fields.size() > 0) {
List<BodyDeclaration> classMembers = cu.getTypes().get(0).getMembers();
int positionOfFirstConstructor = findConstructor(classMembers);
if (positionOfFirstConstructor < 0) {
throw new RuntimeException();
}
classMembers.addAll(positionOfFirstConstructor, fi_fields);
}
if (isSource) {
origSourceToClassName.put(origSource, packageAndClassNameSlash);
} else {
origBytesToClassName.put(origBytes, packageAndClassNameSlash);
}
boolean identicalFound = collectInfoForReports();
if (!identicalFound) { // don't write out identicals
getBaseOutputPath();
if (v2) {
writeV2Orig(source, v3);
}
if (v3) {
String s = cu.toString();
writeV3(s);
}
System.out.print(".");
} else {
System.out.print("d");
}
} catch (ParseException e) {
reportParseException();
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
collectInfoForReports();
System.out.print("d");
}
}
/**
* Add this instance to the tracking information for multiple versions (identical or not) of a class
* @return true if this is an identical duplicate of one already done
*/
private boolean collectInfoForReports() {
String fqcn = (alreadyDone == null) // fully qualified class name with slashes
? packageAndClassNameSlash
: alreadyDone;
// for a given fully qualified class name (slashified),
// find the list of ConvertedSources - one per each different version
// create it if null
List<ConvertedSource> convertedSources = classname2multiSources.get(fqcn);
if (convertedSources == null) {
convertedSources = new ArrayList<>();
classname2multiSources.put(fqcn, convertedSources);
}
// search to see if this instance already in the set
// if so, add the path to the set of identicals
boolean found = false;
for (ConvertedSource cs : convertedSources) {
if ((isSource && cs.rOrigSource.equals(origSource)) ||
(!isSource && Arrays.equals(cs.rOrigBytes, origBytes))) {
cs.add(candidate.p);
found = true;
break;
}
}
if (!found) {
ConvertedSource cs = new ConvertedSource(origSource, origBytes, candidate.p);
convertedSources.add(cs);
}
return found;
}
/******************
* Visitors
******************/
/**
* Capture the type name from all top-level types
* AnnotationDeclaration, Empty, and Enum
*/
@Override
public void visit(AnnotationDeclaration n, Object ignore) {
updateClassName(n);
super.visit(n, ignore);
}
@Override
public void visit(EmptyTypeDeclaration n, Object ignore) {
updateClassName(n);
super.visit(n, ignore);
}
@Override
public void visit(EnumDeclaration n, Object ignore) {
updateClassName(n);
super.visit(n, ignore);
}
/**
* Check if the top level class looks like a JCas class, and report if not:
* has 0, 1, and 2 element constructors
* has static final field defs for type and typeIndexID
*
* Also check if V2 style: 2 arg constructor arg types
* Report if looks like V3 style due to args of 2 arg constructor
*
* if class doesn't extend anything, not a JCas class.
* if class is enum, not a JCas class
* @param n -
* @param ignore -
*/
@Override
public void visit(ClassOrInterfaceDeclaration n, Object ignore) {
// do checks to see if this is a JCas class; if not report skipped
if (n.getParentNode() instanceof CompilationUnit) {
updateClassName(n);
List<ClassOrInterfaceType> supers = n.getExtends();
if (supers == null || supers.size() == 0) {
reportNotJCasClass("class doesn't extend a superclass");
super.visit(n, ignore);
return;
}
List<BodyDeclaration> members = n.getMembers();
setHasJCasConstructors(members);
if (hasV2Constructors && hasTypeFields(members)) {
reportV2Class();
super.visit(n, ignore);
return;
}
if (hasV2Constructors) {
reportNotJCasClassMissingTypeFields();
return;
}
if (hasV3Constructors) {
reportV3Class();
return;
}
reportNotJCasClass("missing v2 constructors");
return;
} else { // do standard processing for non-outer class
super.visit(n, ignore);
return;
}
}
@Override
public void visit(PackageDeclaration n, Object ignored) {
packageName = n.getName().toString();
super.visit(n, ignored);
}
/***************
* Constructors
* - modify the 2 arg constructor - changing the args and the body
* @param n - the constructor node
* @param ignored -
*/
@Override
public void visit(ConstructorDeclaration n, Object ignored) {
super.visit(n, ignored); // processes the params
if (!v3) { // for enums, annotations
return;
}
List<Parameter> ps = n.getParameters();
if (ps.size() == 2 &&
getParmTypeName(ps, 0).equals("int") &&
getParmTypeName(ps, 1).equals("TOP_Type")) {
/** public Foo(TypeImpl type, CASImpl casImpl) {
* super(type, casImpl);
* readObject();
*/
setParameter(ps, 0, "TypeImpl", "type");
setParameter(ps, 1, "CASImpl", "casImpl");
// Body: change the 1st statement (must be super)
List<Statement> stmts = n.getBlock().getStmts();
if (!(stmts.get(0) instanceof ExplicitConstructorInvocationStmt)) {
recordBadConstructor("missing super call");
return;
}
List<Expression> args = ((ExplicitConstructorInvocationStmt)(stmts.get(0))).getArgs();
args.set(0, new NameExpr("type"));
args.set(1, new NameExpr("casImpl"));
// leave the rest unchanged.
}
}
private final static Pattern refGetter = Pattern.compile("(ll_getRef(Array)?Value)|"
+ "(ll_getFSForRef)");
private final static Pattern word1 = Pattern.compile("\\A(\\w*)"); // word chars starting at beginning \\A means beginning
/*****************************
* Method Declaration Visitor
* Heuristic to determine if a feature getter or setter:
* - name: is 4 or more chars, starting with get or set, with 4th char uppercase
* is not "getTypeIndexID"
* - (optional - if comments are available:)
* getter for xxx, setter for xxx
* - for getter: has 0 or 1 arg (1 arg case for indexed getter, arg must be int type)
* - for setter: has 1 or 2 args
*
* Workaround for decompiler - getters which return FSs might be missing the cast to the return value type
*
*****************************/
@Override
public void visit(MethodDeclaration n, Object ignore) {
String name = n.getName();
isGetter = isArraySetter = false;
do { // to provide break exit
if (name.length() >= 4 &&
((isGetter = name.startsWith("get")) || name.startsWith("set")) &&
Character.isUpperCase(name.charAt(3)) &&
!name.equals("getTypeIndexID")) {
List<Parameter> ps = n.getParameters();
if (isGetter) {
if (ps.size() > 1) break;
} else { // is setter
if (ps.size() > 2 ||
ps.size() == 0) break;
if (ps.size() == 2) {
if (!getParmTypeName(ps, 0).equals("int")) break;
isArraySetter = true;
}
}
// get the range-part-name and convert to v3 range ("Ref" changes to "Feature")
String bodyString = n.getBody().toStringWithoutComments();
int i = bodyString.indexOf("jcasType.ll_cas.ll_");
if (i < 0) break;
String s = bodyString.substring(i + "jcasType.ll_cas.ll_get".length()); // also for ...ll_set - same length!
if (s.startsWith("FSForRef(")) { // then it's the wrapper and the wrong instance.
i = s.indexOf("jcasType.ll_cas.ll_");
if (i < 0) {
reportUnrecognizedV2Code("Found \"jcasType.ll_cas.ll_[set or get]...FSForRef(\" but didn't find following \"jcasType.ll_cas_ll_\"\n" + n.toString());
break;
}
s = s.substring(i + "jcasType.ll_cas.ll_get".length());
}
i = s.indexOf("Value");
if (i < 0) {
reportUnrecognizedV2Code("Found \"jcasType.ll_cas.ll_[set or get]\" but didn't find following \"Value\"\n" + n.toString());
break; // give up
}
s = Character.toUpperCase(s.charAt(0)) + s.substring(1, i);
rangeNameV2Part = s;
rangeNamePart = s.equals("Ref") ? "Feature" : s;
// get feat name following ")jcasType).casFeatCode_xxxxx,
i = bodyString.indexOf("jcasType).casFeatCode_");
if (i == -1) {
reportUnrecognizedV2Code("Didn't find \"...jcasType).casFeatCode_\"\n" + n.toString());
break;
}
Matcher m = word1.matcher(bodyString.substring(i + "jcasType).casFeatCode_".length() ));
if (!m.find()) {
reportUnrecognizedV2Code("Found \"...jcasType).casFeatCode_\" but didn't find subsequent word\n" + n.toString());
break;
}
featName = m.group(1);
String fromMethod = Character.toLowerCase(name.charAt(3)) + name.substring(4);
if (!featName.equals(fromMethod)) {
// don't report if the only difference is the first letter captialization
if (!(Character.toLowerCase(featName.charAt(0)) + featName.substring(1)).equals(fromMethod)) {
reportMismatchedFeatureName(String.format("%-25s %s", featName, name));
}
}
List<Expression> args = Collections.singletonList(new StringLiteralExpr(featName));
VariableDeclarator vd = new VariableDeclarator(
new VariableDeclaratorId("_FI_" + featName),
new MethodCallExpr(null, "TypeSystemImpl.getAdjustedFeatureOffset", args));
if (featNames.add(featName)) {
fi_fields.add(new FieldDeclaration(
Modifier.PUBLIC + Modifier.STATIC + Modifier.FINAL,
ASTHelper.INT_TYPE,
vd));
}
/**
* add missing cast stmt for
* return stmts where the value being returned:
* - doesn't have a cast already
* - has the expression be a methodCallExpr with a name which looks like:
* ll_getRefValue or
* ll_getRefArrayValue
*/
if (isGetter && "Feature".equals(rangeNamePart)) {
for (Statement stmt : n.getBody().getStmts()) {
if (stmt instanceof ReturnStmt) {
Expression e = getUnenclosedExpr(((ReturnStmt)stmt).getExpr());
if ((e instanceof MethodCallExpr)) {
String methodName = ((MethodCallExpr)e).getName();
if (refGetter.matcher(methodName).matches()) { // ll_getRefValue or ll_getRefArrayValue
addCastExpr(stmt, n.getType());
}
}
}
}
}
get_set_method = n; // used as a flag during inner "visits" to signal
// we're inside a likely feature setter/getter
} // end of test for getter or setter method
} while (false); // do once, provide break exit
super.visit(n, ignore);
get_set_method = null; // after visiting, reset the get_set_method to null
}
/**
* Visitor for if stmts
* - removes feature missing test
*/
@Override
public void visit(IfStmt n, Object ignore) {
do {
// if (get_set_method == null) break; // sometimes, these occur outside of recogn. getters/setters
Expression c = n.getCondition(), e;
BinaryExpr be, be2;
List<Statement> stmts;
if ((c instanceof BinaryExpr) &&
((be = (BinaryExpr)c).getLeft() instanceof FieldAccessExpr) &&
((FieldAccessExpr)be.getLeft()).getField().equals("featOkTst")) {
// remove the feature missing if statement
// verify the remaining form
if (! (be.getRight() instanceof BinaryExpr)
|| ! ((be2 = (BinaryExpr)be.getRight()).getRight() instanceof NullLiteralExpr)
|| ! (be2.getLeft() instanceof FieldAccessExpr)
|| ! ((e = getExpressionFromStmt(n.getThenStmt())) instanceof MethodCallExpr)
|| ! (((MethodCallExpr)e).getName()).equals("throwFeatMissing")) {
reportDeletedCheckModified("The featOkTst was modified:\n" + n.toString() + '\n');
}
BlockStmt parent = (BlockStmt) n.getParentNode();
stmts = parent.getStmts();
stmts.set(stmts.indexOf(n), new EmptyStmt()); //dont remove
// otherwise iterators fail
// parent.getStmts().remove(n);
return;
}
} while (false);
super.visit(n, ignore);
}
/**
* visitor for method calls
*/
@Override
public void visit(MethodCallExpr n, Object ignore) {
Node p1, p2, updatedNode = null;
List<Expression> args;
do {
if (get_set_method == null) break;
/** remove checkArraybounds statement **/
if (n.getName().equals("checkArrayBounds") &&
((p1 = n.getParentNode()) instanceof ExpressionStmt) &&
((p2 = p1.getParentNode()) instanceof BlockStmt) &&
p2.getParentNode() == get_set_method) {
List<Statement> stmts = ((BlockStmt)p2).getStmts();
stmts.set(stmts.indexOf(p1), new EmptyStmt());
return;
}
// convert simpleCore expression ll_get/setRangeValue
boolean useGetter = isGetter || isArraySetter;
if (n.getName().startsWith("ll_" + (useGetter ? "get" : "set") + rangeNameV2Part + "Value")) {
args = n.getArgs();
if (args.size() != (useGetter ? 2 : 3)) break;
String suffix = useGetter ? "Nc" : rangeNamePart.equals("Feature") ? "NcWj" : "Nfc";
String methodName = "_" + (useGetter ? "get" : "set") + rangeNamePart + "Value" + suffix;
args.remove(0); // remove the old addr arg
// arg 0 converted when visiting args FieldAccessExpr
n.setScope(null);
n.setName(methodName);
}
// convert array sets/gets
String z = "ll_" + (isGetter ? "get" : "set");
if (n.getName().startsWith(z) &&
n.getName().endsWith("ArrayValue")) {
String s = n.getName().substring(z.length());
s = s.substring(0, s.length() - "Value".length()); // s = "ShortArray", etc.
if (s.equals("RefArray")) s = "FSArray";
if (s.equals("IntArray")) s = "IntegerArray";
EnclosedExpr ee = new EnclosedExpr(
new CastExpr(new ClassOrInterfaceType(s), n.getArgs().get(0)));
n.setScope(ee); // the getter for the array fs
n.setName(isGetter ? "get" : "set");
n.getArgs().remove(0);
}
/** remove ll_getFSForRef **/
/** remove ll_getFSRef **/
if (n.getName().equals("ll_getFSForRef") ||
n.getName().equals("ll_getFSRef")) {
updatedNode = replaceInParent(n, n.getArgs().get(0));
}
} while (false);
if (updatedNode != null) {
updatedNode.accept(this, null);
} else {
super.visit(n, null);
}
}
/**
* visitor for field access expressions
* - convert ((...type_Type)jcasType).casFeatCode_XXXX to _FI_xxx
* @param n -
* @param ignore -
*/
@Override
public void visit(FieldAccessExpr n, Object ignore) {
Expression e;
if (get_set_method != null) {
if (n.getField().startsWith("casFeatCode_") &&
((e = getUnenclosedExpr(n.getScope())) instanceof CastExpr) &&
("jcasType".equals(getName(((CastExpr)e).getExpr())))) {
String featureName = n.getField().substring("casFeatCode_".length());
replaceInParent(n, new NameExpr("_FI_" + featureName)); // repl last in List<Expression> (args)
return;
} else if (n.getField().startsWith("casFeatCode_")) {
reportMigrateFailed("Found field casFeatCode_ ... without a previous cast expr using jcasType");
}
}
super.visit(n, ignore);
}
private class removeEmptyStmts extends VoidVisitorAdapter<Object> {
@Override
public void visit(BlockStmt n, Object ignore) {
Iterator<Statement> it = n.getStmts().iterator();
while (it.hasNext()) {
if (it.next() instanceof EmptyStmt) {
it.remove();
}
}
super.visit(n, ignore);
}
// @Override
// public void visit(MethodDeclaration n, Object ignore) {
// if (n.getName().equals("getModifiablePrimitiveNodes")) {
// System.out.println("debug");
// }
// super.visit(n, ignore);
// if (n.getName().equals("getModifiablePrimitiveNodes")) {
// System.out.println("debug");
// }
// }
}
/**
* converted files:
* java name, path (sorted by java name, v3 name only)
* not-converted:
* java name, path (sorted by java name)
* duplicates:
* java name, path (sorted by java name)
* @return true if it's likely everything converted OK.
*/
private boolean report() {
System.out.println("\n\nMigration Summary");
System.out.format("Output top directory: %s%n", outputDirectory);
System.out.format("Date/time: %tc%n", new Date());
pprintRoots("Sources", sourcesRoots);
pprintRoots("Classes", classesRoots);
boolean isOk = true;
try {
isOk = isOk && reportPaths("Workaround Directories", "workaroundDir.txt", pathWorkaround);
isOk = isOk && reportPaths("Reports of converted files where a deleted check was customized", "deletedCheckModified.txt", deletedCheckModified);
isOk = isOk && reportPaths("Reports of converted files needing manual inspection", "manualInspection.txt", manualInspection);
isOk = isOk && reportPaths("Reports of files which failed migration", "failed.txt", failedMigration);
isOk = isOk && reportPaths("Reports of non-JCas files", "NonJCasFiles.txt", nonJCasFiles);
isOk = isOk && reportPaths("Builtin JCas classes - skipped - need manual checking to see if they are modified",
"skippedBuiltins.txt", skippedBuiltins);
// can't do the pear report here - post processing not yet done.
// computeDuplicates();
// reportPaths("Report of duplicates - not identical", "nonIdenticalDuplicates.txt", nonIdenticalDuplicates);
// reportPaths("Report of duplicates - identical", "identicalDuplicates.txt", identicalDuplicates);
isOk = isOk && reportDuplicates();
reportPaths("Report of processed files", "processed.txt", c2ps);
return isOk;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void pprintRoots(String kind, String[] roots) {
if (roots.length > 0) {
System.out.println(kind + " Roots:");
for (String s : roots) {
System.out.format(" %s%n", s);
}
System.out.println("\n");
}
}
// private void computeDuplicates() {
// List<ClassnameAndPath> toCheck = new ArrayList<>(c2ps);
// toCheck.addAll(extendableBuiltins);
// sortReport2(toCheck);
// ClassnameAndPath prevP = new ClassnameAndPath(null, null);
// List<ClassnameAndPath> sameList = new ArrayList<>();
// boolean areAllEqual = true;
//
// for (ClassnameAndPath p : toCheck) {
// if (!p.getFirst().equals(prevP.getFirst())) {
//
// addToIdenticals(sameList, areAllEqual);
// sameList.clear();
// areAllEqual = true;
//
// prevP = p;
// continue;
// }
//
// // have 2nd or subsequent same class
// if (sameList.size() == 0) {
// sameList.add(prevP);
// }
// sameList.add(p);
// if (areAllEqual) {
// if (isFilesMiscompare(p.path, prevP.path)) {
// areAllEqual = false;
// }
// }
// }
//
// addToIdenticals(sameList, areAllEqual);
// }
// /**
// * Compare two java source or class files
// * @param p1
// * @param p2
// * @return
// */
// private boolean isFilesMiscompare(Path p1, Path p2) {
// String s1 = getSource(p1);
// String s2 = getSource(p2);
// return !s1.equals(s2);
// }
// private void addToIdenticals(List<ClassnameAndPath> sameList, boolean areAllEqual) {
// if (sameList.size() > 0) {
// if (areAllEqual) {
// identicalDuplicates.addAll(sameList);
// } else {
// nonIdenticalDuplicates.addAll(sameList);
// }
// }
// }
private Path makePath(String name) throws IOException {
Path p = Paths.get(name);
Path parent = p.getParent();
try {
Files.createDirectories(parent);
} catch (FileAlreadyExistsException e) {
// caused by running on Windows system which ignores "case"
// there's a file at /x/y/ named "z", but the path wants to be /x/y/Z/
// Workaround: change "z" to "z_c" c for capitalization issue
String newDir = parent.getFileName().toString() + "_c";
Path p2 = Paths.get(parent.getParent().toString(), newDir);
Files.createDirectories(p2);
reportPathWorkaround(parent.toString(), p2.toString());
return Paths.get(p2.toString(), p.getFileName().toString());
}
return p;
}
private boolean reportDuplicates() {
List<Entry<String, List<ConvertedSource>>> nonIdenticals = new ArrayList<>();
List<ConvertedSource> onlyIdenticals = new ArrayList<>();
for (Entry<String, List<ConvertedSource>> e : classname2multiSources.entrySet()) {
List<ConvertedSource> convertedSourcesFor1class = e.getValue();
if (convertedSourcesFor1class.size() > 1) {
// have multiple non-identical sources for one class
nonIdenticals.add(e);
} else {
ConvertedSource cs = convertedSourcesFor1class.get(0);
if (cs.paths.size() > 1) {
// have multiple (only) identical sources for one class
onlyIdenticals.add(cs);
}
}
}
if (nonIdenticals.size() == 0) {
if (onlyIdenticals.size() == 0) {
System.out.println("There were no duplicates found.");
} else {
// report identical duplicates
System.out.println("Identical duplicates (only):");
for (ConvertedSource cs : onlyIdenticals) {
for (Path path : cs.paths) {
System.out.println(" " + vWithFileSys(path));
}
}
}
return true;
} else {
System.out.println("Report of non-identical duplicates");
for (Entry<String, List<ConvertedSource>> nonIdentical : nonIdenticals) {
System.out.println(" classname: " + nonIdentical.getKey());
int i = 1;
for (ConvertedSource cs : nonIdentical.getValue()) {
System.out.println(" version " + i);
for (Path path : cs.paths) {
System.out.println(" " + vWithFileSys(path));
}
i++;
}
}
}
return false;
}
/**
*
* @param title -
* @param fileName -
* @param items -
* @return true if likely ok
* @throws IOException -
*/
private <T, U> boolean reportPaths(String title, String fileName, List<? extends Report2<T, U>> items) throws IOException {
if (items.size() == 0) {
System.out.println("There were no " + title);
return true;
}
System.out.println("\n" + title);
for (int i = 0; i < title.length(); i++) System.out.print('=');
System.out.println("");
try (BufferedWriter bw = Files.newBufferedWriter(makePath(outDirLog + fileName), StandardOpenOption.CREATE)) {
List<Report2<T, U>> sorted = new ArrayList<>(items);
sortReport2(sorted);
int max = 0;
for (Report2<T, U> p : sorted) {
max = Math.max(max, p.getFirstLength());
}
int i = 1;
for (Report2<T, U> p : sorted) {
Object v = p.getSecond();
String s = String.format("%5d %-" +max+ "s %s%n", Integer.valueOf(i), p.getFirst(), vWithFileSys(v));
bw.write(s);
System.out.print(s);
i++;
}
System.out.println("");
} // end of try-with-resources
return false;
}
private String vWithFileSys(Object v) {
if (v instanceof Path) {
Path path = (Path) v;
FileSystem fileSystem = path.getFileSystem();
if (fileSystem instanceof com.sun.nio.zipfs.ZipFileSystem) {
v = v.toString() + "\t\t " + fileSystem.toString();
}
}
return v.toString();
}
private <T, U> void sortReport2(List<? extends Report2<T, U>> items) {
Collections.sort(items,
(o1, o2) -> {
int r = o1.getFirst().compareTo((T) o2.getFirst());
if (r == 0) {
r = o1.getSecond().compareTo((U) o2.getSecond());
}
return r;
});
}
/**
* Walk the directory tree rooted at root
* - descend subdirectories
* - descend Jar file
* -- descend nested Jar files (!)
* by extracting these to a temp dir, and keeping a back reference to where they were extracted from.
*
* output the paths representing the classes
* - the path includes the "file system".
* @param root
* @return list of Paths to walk
* @throws IOException
*/
private List<Candidate> getCandidates(CommandLineParser clp, String root) {
isSkipTypeCheck = clp.isInArgsList(SKIP_TYPE_CHECK);
Path startPath = Paths.get(root);
candidates = new ArrayList<>();
try (Stream<Path> stream = Files.walk(startPath, FileVisitOption.FOLLOW_LINKS)) { // needed to release file handles
stream.forEachOrdered(
p -> getCandidates_processFile(p, null));
} catch (IOException e) {
throw new RuntimeException(e);
}
Collections.sort(candidates, pathComparator);
// the collection potentially includes inner class files
List<Candidate> c = new ArrayList<>(); // candidates
final int nbrOfPaths = candidates.size();
if (nbrOfPaths == 0) {
return c;
}
for (int i = 0; i < nbrOfPaths; i++) {
// skip files that end with _Type or
// appear to be inner files: have names with a "$" char
candidate = candidates.get(i);
String lastPartOfPath = candidate.p.getFileName().toString();
if (lastPartOfPath.endsWith(isSource ? "_Type.java" : "_Type.class")) {
continue; // skip the _Type files
}
if (lastPartOfPath.contains("$")) {
continue; // inner class
}
if (isSkipTypeCheck) {
c.add(candidate);
} else {
// doing _Type check: only include java files if there's an associated _Type file
// in the sort order, these follow the file without the _Type suffix
// but perhaps there are other names inbetween
boolean has_Type = has_Type(candidate, i + 1); // look for the next _Type file starting at position i + 1
if (!has_Type) {
continue; // not a JCas class
}
c.add(candidate);
}
}
return c;
}
private final static Comparator<Candidate> pathComparator = new Comparator<Candidate>() {
@Override
public int compare(Candidate o1, Candidate o2) {
return o1.p.toString().compareTo(o2.p.toString());
}
};
// // there may be several same-name roots not quite right
// // xxx_Type$1.class
//
// private void addIfPreviousIsSameName(List<Path> c, int i) {
// if (i == 0) return;
// String _Type = candidates.get(i).toString();
//// String prev = r.get(i-1).getPath();
// String prefix = _Type.substring(0, _Type.length() - ("_Type." + (isSource ? "java" : "class")).length());
// i--;
// while (i >= 0) {
// String s = candidates.get(i).toString();
// if ( ! s.startsWith(prefix)) {
// break;
// }
// if (s.substring(prefix.length()).equals((isSource ? ".java" : ".class"))) {
// c.add(candidates.get(i));
// break;
// }
// i--;
// }
// }
/**
* adds all the .java or .class files to the candidates, including _Type if not skipping the _Type check
* Handling embedded jar files
* - single level Jar (at the top level of the default file system)
* -- handle using an overlayed file system
* - embedded Jars within Jars:
* - not supported by Zip File System Provider (it only supports one level)
* - handle by extracting to a temp dir, and then using the Zip File System Provider
* @param path the path to a .java or .class or .jar or .pear
* @param pearClasspath - a string representing a path to the pear's classpath if there is one, or null
*/
private void getCandidates_processFile(Path path, String pearClasspath) {
// System.out.println("debug processing " + path);
try {
// URI pathUri = path.toUri();
String pathString = path.toString();
final boolean isPear = pathString.endsWith(".pear");
final boolean isJar = pathString.endsWith(".jar");
if (isJar || isPear) { // path.endsWith does not mean this !!
if (!path.getFileSystem().equals(FileSystems.getDefault())) {
// embedded Jar: extract to temp
Path out = getTempOutputPath(path);
Files.copy(path, out, StandardCopyOption.REPLACE_EXISTING);
// embeddedJars.add(new PathAndPath(path, out));
path = out;
}
Path start;
final String localPearClasspath;
if (isPear) {
if (pearClasspath != null) {
throw new UIMARuntimeException("Nested PEAR files not supported");
}
pear_current = new PearOrJar(path);
pears.add(pear_current);
// add pear classpath info
File pearInstallDir = Files.createTempDirectory(getTempDir(), "installedPear").toFile();
PackageBrowser ip = PackageInstaller.installPackage(pearInstallDir, path.toFile(), false);
localPearClasspath = ip.buildComponentClassPath();
String[] children = pearInstallDir.list();
if (children == null || children.length != 1) {
Misc.internalError();
}
pearResolveStart = Paths.get(pearInstallDir.getAbsolutePath(), children[0]);
start = pearInstallDir.toPath();
} else {
if (isJar) {
PearOrJar jarInfo = new PearOrJar(path);
jar_current_stack.push(jarInfo);
jars.add(jarInfo);
}
localPearClasspath = pearClasspath;
FileSystem jfs = FileSystems.newFileSystem(Paths.get(path.toUri()), null);
start = jfs.getPath("/");
}
try (Stream<Path> stream = Files.walk(start)) { // needed to release file handles
stream.forEachOrdered(
p -> getCandidates_processFile(p, localPearClasspath));
}
if (isJar) {
jar_current_stack.pop();
}
if (isPear) {
pear_current = null;
}
} else {
// is not a .jar or .pear file. add .java or .class files to initial candidate set
// will be filtered additionally later
// System.out.println("debug path ends with java or class " + pathString.endsWith(isSource ? ".java" : ".class") + " " + pathString);
if (pathString.endsWith(isSource ? ".java" : ".class")) {
candidates.add(new Candidate(path, pearClasspath));
if (!isSource && null != pear_current) {
// inside a pear, which has been unzipped into pearInstallDir;
path2InsidePearOrJarPath.put(path.toString(), pearResolveStart.relativize(path).toString());
pear_current.pathsToCandidateFiles.add(path.toString());
}
if (!isSource && jar_current_stack.size() > 0) {
// inside a jar, not contained in a pear
jar_current_stack.getFirst().pathsToCandidateFiles.add(path.toString());
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
*
* @param path used to get the last name from the path
* @return a temporary file in the local temp directory that has some name parts from the path's file, and ends in .jar
* @throws IOException -
*/
private Path getTempOutputPath(Path path) throws IOException {
Path localTempDir = getTempDir();
Path tempFile = Files.createTempFile(localTempDir, path.getFileName().toString(), ".jar");
tempFile.toFile().deleteOnExit();
return tempFile;
}
private Path getTempDir() throws IOException {
if (tempDir == null) {
tempDir = Files.createTempDirectory("migrateJCas");
tempDir.toFile().deleteOnExit();
}
return tempDir;
}
private boolean has_Type(Candidate cand, int start) {
if (start >= candidates.size()) {
return false;
}
String sc = cand.p.toString();
String sc_minus_suffix = sc.substring(0, sc.length() - ( isSource ? ".java".length() : ".class".length()));
String sc_Type = sc_minus_suffix + ( isSource ? "_Type.java" : "_Type.class");
// a string which sorts beyond the candidate + a suffix of "_"
String s_end = sc_minus_suffix + (char) (((int)'_') + 1);
for (Candidate c : candidates.subList(start, candidates.size())) {
String s = c.p.toString();
if (s_end.compareTo(s) < 0) {
return false; // not found, we're already beyond where it would be found
}
if (s.equals(sc_Type)) {
return true;
}
}
return false;
}
private static final CommandLineParser createCmdLineParser() {
CommandLineParser parser = new CommandLineParser();
parser.addParameter(SOURCE_FILE_ROOTS, true);
parser.addParameter(CLASS_FILE_ROOTS, true);
parser.addParameter(OUTPUT_DIRECTORY, true);
parser.addParameter(SKIP_TYPE_CHECK, false);
parser.addParameter(MIGRATE_CLASSPATH, true);
parser.addParameter(CLASSES, true);
return parser;
}
private final boolean checkCmdLineSyntax(CommandLineParser clp) {
if (clp.getRestArgs().length > 0) {
System.err.println("Error parsing CVD command line: unknown argument(s):");
String[] args = clp.getRestArgs();
for (int i = 0; i < args.length; i++) {
System.err.print(" ");
System.err.print(args[i]);
}
System.err.println();
return false;
}
if (clp.isInArgsList(SOURCE_FILE_ROOTS) && clp.isInArgsList(CLASS_FILE_ROOTS)) {
System.err.println("both sources file roots and classess file roots parameters specified; please specify just one.");
return false;
}
if (clp.isInArgsList(OUTPUT_DIRECTORY)) {
outputDirectory = Paths.get(clp.getParamArgument(OUTPUT_DIRECTORY)).toString();
if (!outputDirectory.endsWith("/")) {
outputDirectory = outputDirectory + "/";
}
outDirConverted = outputDirectory + "converted/";
outDirSkipped = outputDirectory + "not-converted/";
outDirLog = outputDirectory + "logs/";
} else {
System.err.println("-outputDirectory is a required parameter, must be a path to a writable file directory.");
return false;
}
if (clp.isInArgsList(MIGRATE_CLASSPATH)) {
migrateClasspath = clp.getParamArgument(MIGRATE_CLASSPATH);
}
if (clp.isInArgsList(CLASSES)) {
individualClasses = clp.getParamArgument(CLASSES);
}
return true;
}
private String decompile(byte[] b, String pearClasspath) {
ClassLoader cl = getClassLoader(pearClasspath);
UimaDecompiler ud = new UimaDecompiler(cl, null);
return ud.decompile(b);
}
private String decompile(String classname) {
ClassLoader cl = getClassLoader(null);
UimaDecompiler ud = new UimaDecompiler(cl, null);
return ud.decompileToString(classname);
}
// private String getFullyQualifiedClassNameWithSlashes(byte[] b) {
// ClassLoader cl = getClassLoader();
// UimaDecompiler ud = new UimaDecompiler(cl, null);
// return ud.extractClassNameSlashes(b);
// }
/**
* The classloader to use in decompiling, if it is provided, is one that delegates first
* to the parent. This may need fixing for PEARs
* @return classloader to use for migrate decompiling
*/
private ClassLoader getClassLoader(String pearClasspath) {
if (null == pearClasspath) {
if (null == cachedMigrateClassLoader) {
cachedMigrateClassLoader = (migrateClasspath == null)
? this.getClass().getClassLoader()
: new URLClassLoader(Misc.classpath2urls(migrateClasspath));
}
return cachedMigrateClassLoader;
} else {
try {
return new UIMAClassLoader((null == migrateClasspath)
? pearClasspath
: (pearClasspath + File.pathSeparator + migrateClasspath));
} catch (MalformedURLException e) {
throw new UIMARuntimeException(e);
}
}
}
private void addImport(String s) {
cu.getImports().add(new ImportDeclaration(new NameExpr(s), false, false));
}
private void removeImport(String s) {
Iterator<ImportDeclaration> it = cu.getImports().iterator();
while (it.hasNext()) {
ImportDeclaration impDcl = it.next();
if (impDcl.getName().toString().equals(s)) {
it.remove();
break;
}
}
}
/******************
* AST Utilities
******************/
private Node replaceInParent(Node n, Expression v) {
Node parent = n.getParentNode();
if (parent instanceof EnclosedExpr) {
((EnclosedExpr)parent).setInner(v);
} else if (parent instanceof MethodCallExpr) { // args in the arg list
List<Expression> args = ((MethodCallExpr)parent).getArgs();
args.set(args.indexOf(n), v);
v.setParentNode(parent);
} else if (parent instanceof ExpressionStmt) {
((ExpressionStmt)parent).setExpression(v);
} else if (parent instanceof CastExpr) {
((CastExpr)parent).setExpr(v);
} else if (parent instanceof ReturnStmt) {
((ReturnStmt)parent).setExpr(v);
} else if (parent instanceof AssignExpr) {
((AssignExpr)parent).setValue(v);
} else if (parent instanceof VariableDeclarator) {
((VariableDeclarator)parent).setInit(v);
} else if (parent instanceof ObjectCreationExpr) {
List<Expression> args = ((ObjectCreationExpr)parent).getArgs();
int i = args.indexOf(n);
if (i < 0) throw new RuntimeException();
args.set(i, v);
} else {
System.out.println(parent.getClass().getName());
throw new RuntimeException();
}
return v;
}
/**
*
* @param p the parameter to modify
* @param t the name of class or interface
* @param name the name of the variable
*/
private void setParameter(List<Parameter> ps, int i, String t, String name) {
Parameter p = ps.get(i);
p.setType(new ClassOrInterfaceType(t));
p.setId(new VariableDeclaratorId(name));
}
private int findConstructor(List<BodyDeclaration> classMembers) {
int i = 0;
for (BodyDeclaration bd : classMembers) {
if (bd instanceof ConstructorDeclaration) {
return i;
}
i++;
}
return -1;
}
private boolean hasTypeFields(List<BodyDeclaration> members) {
boolean hasType = false;
boolean hasTypeId = false;
for (BodyDeclaration bd : members) {
if (bd instanceof FieldDeclaration) {
FieldDeclaration f = (FieldDeclaration)bd;
int m = f.getModifiers();
if (Modifier.isPublic(m) &&
Modifier.isStatic(m) &&
Modifier.isFinal(m) &&
getTypeName(f.getType()).equals("int")) {
List<VariableDeclarator> vds = f.getVariables();
for (VariableDeclarator vd : vds) {
String n = vd.getId().getName();
if (n.equals("type")) hasType = true;
if (n.equals("typeIndexID")) hasTypeId = true;
if (hasTypeId && hasType) break;
}
}
}
} // end of for
return hasTypeId && hasType;
}
/**
* Heuristic:
* JCas classes have 0, 1, and 2 arg constructors with particular arg types
* 0 -
* 1 - JCas
* 2 - int, TOP_Type (v2) or TypeImpl, CASImpl (v3)
*
* Additional 1 and 2 arg constructors are permitted.
*
* Sets fields hasV2Constructors, hasV3Constructors
* @param members
*/
private void setHasJCasConstructors(List<BodyDeclaration> members) {
boolean has0ArgConstructor = false;
boolean has1ArgJCasConstructor = false;
boolean has2ArgJCasConstructorV2 = false;
boolean has2ArgJCasConstructorV3 = false;
for (BodyDeclaration bd : members) {
if (bd instanceof ConstructorDeclaration) {
List<Parameter> ps = ((ConstructorDeclaration)bd).getParameters();
if (ps.size() == 0) has0ArgConstructor = true;
if (ps.size() == 1 && getParmTypeName(ps, 0).equals("JCas")) {
has1ArgJCasConstructor = true;
}
if (ps.size() == 2) {
if (getParmTypeName(ps, 0).equals("int") &&
getParmTypeName(ps, 1).equals("TOP_Type")) {
has2ArgJCasConstructorV2 = true;
} else if (getParmTypeName(ps, 0).equals("TypeImpl") &&
getParmTypeName(ps, 1).equals("CASImpl")) {
has2ArgJCasConstructorV3 = true;
}
} // end of 2 arg constructor
} // end of is-constructor
} // end of for loop
hasV2Constructors = has0ArgConstructor && has1ArgJCasConstructor && has2ArgJCasConstructorV2;
hasV3Constructors = has0ArgConstructor && has1ArgJCasConstructor && has2ArgJCasConstructorV3;
}
private String getParmTypeName(List<Parameter> p, int i) {
return getTypeName(p.get(i).getType());
}
private String getTypeName(Type t) {
if (t instanceof ReferenceType) {
t = ((ReferenceType)t).getType();
}
if (t instanceof PrimitiveType) {
return ((PrimitiveType)t).toString();
}
if (t instanceof ClassOrInterfaceType) {
return ((ClassOrInterfaceType)t).getName();
}
Misc.internalError(); return null;
}
/**
* Get the name of a field
* @param e -
* @return the field name or null
*/
private String getName(Expression e) {
e = getUnenclosedExpr(e);
if (e instanceof NameExpr) {
return ((NameExpr)e).getName();
}
if (e instanceof FieldAccessExpr) {
return ((FieldAccessExpr)e).getField();
}
return null;
}
/**
* Called on Annotation Decl, Class/intfc decl, empty type decl, enum decl
* Does nothing unless at top level of compilation unit
*
* Otherwise, adds an entry to c2ps for the classname and package, plus full path
*
* @param n type being declared
*/
private void updateClassName(TypeDeclaration n) {
if (n.getParentNode() instanceof CompilationUnit) {
className = n.getName();
String packageAndClassName =
(className.contains("."))
? className
: packageName + '.' + className;
packageAndClassNameSlash = packageAndClassName.replace('.', '/');
TypeImpl ti = TypeSystemImpl.staticTsi.getType(Misc.javaClassName2UimaTypeName(packageAndClassName));
if (null != ti) {
// is a built-in type
ClassnameAndPath p = new ClassnameAndPath(packageAndClassNameSlash, candidate.p);
skippedBuiltins.add(p);
v3 = false; // skip further processing of this class
return;
}
c2ps.add(new ClassnameAndPath(packageAndClassNameSlash, candidate.p));
path2classname.put(candidate.p.toString(), packageAndClassNameSlash);
return;
}
return;
}
private Expression getExpressionFromStmt(Statement stmt) {
stmt = getStmtFromStmt(stmt);
if (stmt instanceof ExpressionStmt) {
return getUnenclosedExpr(((ExpressionStmt)stmt).getExpression());
}
return null;
}
private Expression getUnenclosedExpr(Expression e) {
while (e instanceof EnclosedExpr) {
e = ((EnclosedExpr)e).getInner();
}
return e;
}
/**
* Unwrap (possibly nested) 1 statement blocks
* @param stmt -
* @return unwrapped (non- block) statement
*/
private Statement getStmtFromStmt(Statement stmt) {
while (stmt instanceof BlockStmt) {
List<Statement> stmts = ((BlockStmt) stmt).getStmts();
if (stmts.size() == 1) {
stmt = stmts.get(0);
continue;
}
return null;
}
return stmt;
}
private void addCastExpr(Statement stmt, Type castType) {
ReturnStmt rstmt = (ReturnStmt) stmt;
rstmt.setExpr(new CastExpr(castType, rstmt.getExpr()));
}
/********************
* Recording results
********************/
private void recordBadConstructor(String msg) {
reportMigrateFailed("Constructor is incorrect, " + msg);
}
private void reportParseException() {
reportMigrateFailed("Unparsable Java");
}
private void migrationFailed(String reason) {
failedMigration.add(new PathAndReason(candidate.p, reason));
v3 = false;
}
private void reportMigrateFailed(String m) {
System.out.format("Skipping this file due to error: %s, path: %s%n", m, candidate);
migrationFailed(m);
}
private void reportV2Class() {
v2JCasFiles.add(candidate.p);
v2 = true;
}
private void reportV3Class() {
v3JCasFiles.add(candidate.p);
v3 = true;
}
private void reportNotJCasClass(String reason) {
nonJCasFiles.add(new PathAndReason(candidate.p, reason));
v3 = false;
}
private void reportNotJCasClassMissingTypeFields() {
reportNotJCasClass("missing required type and/or typeIndexID static fields");
}
private void reportDeletedCheckModified(String m) {
deletedCheckModified.add(new PathAndReason(candidate.p, m));
}
private void reportMismatchedFeatureName(String m) {
manualInspection.add(new PathAndReason(candidate.p, "This getter/setter name doesn't match internal feature name: " + m));
}
private void reportUnrecognizedV2Code(String m) {
migrationFailed("V2 code not recognized:\n" + m);
}
private void reportPathWorkaround(String orig, String modified) {
pathWorkaround.add(new String1AndString2(orig, modified));
}
private void reportPearOrJarClassReplace(String pearOrJar, String classname, String kind) { // pears or jars
if (kind.equals("pears")) {
pearClassReplace.add(new String1AndString2(pearOrJar, classname));
} else {
jarClassReplace.add(new String1AndString2(pearOrJar, classname));
}
}
/***********************************************/
private void getBaseOutputPath() {
String s = packageAndClassNameSlash;
int i = 0;
while (!usedPackageAndClassNames.add(s)) {
i = i + 1;
s = packageAndClassNameSlash + "_dupid_" + i;
}
packageAndClassNameSlash_i = i;
}
private String getBaseOutputPath(boolean wasConverted, boolean isV2) {
return (wasConverted ? outDirConverted : outDirSkipped) + (isV2 ? "v2/" : "v3/")
+ ((packageAndClassNameSlash_i > 0)
? (Integer.toString(packageAndClassNameSlash_i) + "/")
: "")
+ packageAndClassNameSlash
+ ".java";
}
private void writeV2Orig(String data, boolean wasConverted) throws IOException {
String base = getBaseOutputPath(wasConverted, true); // adds numeric suffix if dupls
FileUtils.writeToFile(makePath(base), data);
}
private void writeV3(String data) throws IOException {
String base = getBaseOutputPath(true, false); // adds numeric suffix if dupls
data = fixImplementsBug(data);
FileUtils.writeToFile(makePath(base), data);
}
private void printUsage() {
System.out.println(
"Usage: java org.apache.uima.migratev3.jcas.MigrateJCas \n"
+ " [-sourcesRoots <One-or-more-directories-or-jars-separated-by-Path-separator>]\n"
+ " [-classesRoots <One-or-more-directories-or-jars-or-pears-separated-by-Path-separator>]\n"
+ " [-classes <one-or-more-fully-qualified-class-names-separated-by-Path-separator]\n"
+ " example: -classes mypkg.foo:pkg2.bar\n"
+ " [-outputDirectory a-writable-directory-path (required)\n"
+ " [-migrateClasspath a-class-path to use in decompiling, used if -classesRoots is specified\n"
+ " also used when compiling the migrated classes.\n"
+ " PEAR processing augments this with the PEAR's classpath information "
+ " [-skipTypeCheck if specified, skips validing a found item by looking for the corresponding _Type file"
+ " NOTE: either -sourcesRoots or -classesRoots is required, but only one may be specified.\n"
+ " NOTE: classesRoots are scanned for JCas classes, which are then decompiled, and the results processed like sourcesRoots\n"
+ " The decompiling requires that the classes being scanned be on the migrateClasspath when this is invoked.\n"
+ " NOTE: -outputDirectory is required\n");
}
private static final Pattern implementsEmpty = Pattern.compile("implements \\{");
private String fixImplementsBug(String data) {
return implementsEmpty.matcher(data).replaceAll("{");
}
// /**
// * Called after class is migrated
// * Given a path to a class (source or class file),
// * return the URL to the class as found in the classpath.
// * This returns the "first" one found in the classpath, in the case of duplicates.
// * @param path
// * @return the location of the class in the class path
// */
// private URL getPathForClass(Path path) {
// return (null == packageAndClassNameSlash)
// ? null
// : migrateClassLoader.getResource(packageAndClassNameSlash + ".class");
// }
/**
* prepare to migrate one class
*/
private String prepare(Candidate c) {
candidate = c;
packageName = null;
className = null;
packageAndClassNameSlash = null;
cu = null;
return getSource(c);
}
private String prepareIndividual(String classname) {
candidate = new Candidate(Paths.get(classname)); // a pseudo path
packageName = null;
className = null;
packageAndClassNameSlash = null;
cu = null;
return decompile(classname); // always look up in classpath
// to decompile individual source - put in sourcesRoots
}
/*********************************************************************
* Reporting classes
*********************************************************************/
private static abstract class Report2<T, U> {
public abstract Comparable<T> getFirst(); // Eclipse on linux complained if not public, was OK on windows
public abstract Comparable<U> getSecond();
abstract int getFirstLength();
}
private static class PathAndReason extends Report2<Path, String> {
Path path;
String reason;
PathAndReason(Path path, String reason) {
this.path = path;
this.reason = reason;
}
@Override
public Comparable<Path> getFirst() { return path; }
@Override
public Comparable<String> getSecond() { return reason; }
@Override
int getFirstLength() { return path.toString().length(); }
}
private static class ClassnameAndPath extends Report2<String, Path> {
String classname;
Path path;
ClassnameAndPath(String classname, Path path) {
this.classname = classname;
this.path = path;
}
@Override
public Comparable<String> getFirst() { return classname; }
@Override
public Comparable<Path> getSecond() { return path; }
@Override
int getFirstLength() { return classname.length(); }
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((classname == null) ? 0 : classname.hashCode());
result = prime * result + ((path == null) ? 0 : path.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof ClassnameAndPath))
return false;
ClassnameAndPath other = (ClassnameAndPath) obj;
if (classname == null) {
if (other.classname != null)
return false;
} else if (!classname.equals(other.classname))
return false;
if (path == null) {
if (other.path != null)
return false;
} else if (!path.equals(other.path))
return false;
return true;
}
}
private static class String1AndString2 extends Report2<String, String> {
String s1;
String s2;
String1AndString2(String s1, String s2) {
this.s1 = s1;
this.s2 = s2;
}
@Override
public Comparable<String> getFirst() { return s1; }
@Override
public Comparable<String> getSecond() { return s2; }
@Override
int getFirstLength() { return s1.toString().length(); }
}
}