org.sahagin.runlib.srctreegen.SrcTreeGenerator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sahagin Show documentation
Show all versions of sahagin Show documentation
Sahagin makes your Selenium script more readable and maintainable.
package org.sahagin.runlib.srctreegen;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.jar.Manifest;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.FileASTRequestor;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.sahagin.runlib.additionaltestdoc.AdditionalClassTestDoc;
import org.sahagin.runlib.additionaltestdoc.AdditionalFuncTestDoc;
import org.sahagin.runlib.additionaltestdoc.AdditionalPage;
import org.sahagin.runlib.additionaltestdoc.AdditionalTestDocs;
import org.sahagin.runlib.external.CaptureStyle;
import org.sahagin.runlib.external.adapter.AdapterContainer;
import org.sahagin.share.CommonUtils;
import org.sahagin.share.IllegalTestScriptException;
import org.sahagin.share.Logging;
import org.sahagin.share.srctree.PageClass;
import org.sahagin.share.srctree.SrcTree;
import org.sahagin.share.srctree.TestClass;
import org.sahagin.share.srctree.TestClassTable;
import org.sahagin.share.srctree.TestFuncTable;
import org.sahagin.share.srctree.TestFunction;
import org.sahagin.share.srctree.TestMethod;
import org.sahagin.share.srctree.code.Code;
import org.sahagin.share.srctree.code.CodeLine;
import org.sahagin.share.srctree.code.FuncArgument;
import org.sahagin.share.srctree.code.StringCode;
import org.sahagin.share.srctree.code.SubMethodInvoke;
import org.sahagin.share.srctree.code.UnknownCode;
public class SrcTreeGenerator {
private static Logger logger = Logging.getLogger(SrcTreeGenerator.class.getName());
private AdditionalTestDocs additionalTestDocs;
// additionalTestDocs can be null
public SrcTreeGenerator(AdditionalTestDocs additionalTestDocs) {
if (additionalTestDocs == null) {
// use empty additionalTestDocs
this.additionalTestDocs = new AdditionalTestDocs();
} else {
this.additionalTestDocs = additionalTestDocs;
}
}
// result first value .. TestDoc value. return null if no TestDoc found
// result second value.. isPage
private Pair getTestDoc(ITypeBinding type) {
// Page testDoc is prior to TestDoc value
String pageTestDoc = ASTUtils.getPageTestDoc(type);
if (pageTestDoc != null) {
return Pair.of(pageTestDoc, true);
}
String testDoc = ASTUtils.getTestDoc(type);
if (testDoc != null) {
return Pair.of(testDoc, false);
}
AdditionalClassTestDoc additional
= additionalTestDocs.getClassTestDoc(type.getQualifiedName());
if (additional != null) {
return Pair.of(additional.getTestDoc(), additional instanceof AdditionalPage);
}
return Pair.of(null, false);
}
// value (return null if no TestDoc found) and stepInCapture value pair
private Pair getTestDoc(IMethodBinding method) {
Pair pair = ASTUtils.getTestDoc(method);
if (pair != null) {
return pair;
}
AdditionalFuncTestDoc additional
= additionalTestDocs.getFuncTestDoc(ASTUtils.qualifiedMethodName(method));
if (additional != null) {
return Pair.of(additional.getTestDoc(), additional.getCaptureStyle());
}
return null;
}
private boolean isSubFunction(IMethodBinding methodBinding) {
// rootFuntion also can have its TestDoc value
if (AdapterContainer.globalInstance().isRootFunction(methodBinding)) {
return false;
}
return getTestDoc(methodBinding) != null;
}
// srcFiles..parse target files
// classPathEntries.. all class paths (class file containing directory or jar file) srcFiles depend
private static void parseAST(
String[] srcFiles, String srcEncoding, String[] classPathEntries, FileASTRequestor requestor) {
ASTParser parser = ASTParser.newParser(AST.JLS4);
Map, ?> options = JavaCore.getOptions();
JavaCore.setComplianceOptions(JavaCore.VERSION_1_5, options);
parser.setCompilerOptions(options);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(true);
parser.setBindingsRecovery(true);
parser.setEnvironment(classPathEntries, null, null, true);
String[] srcEncodings = new String[srcFiles.length];
for (int i = 0; i < srcEncodings.length; i++) {
srcEncodings[i] = srcEncoding;
}
parser.createASTs(
srcFiles, srcEncodings, new String[]{}, requestor, null);
}
// Get all root methods and its class as TestRootClass.
// Each TestRootClass owns only TestRootMethod, and TestSubMethod information is not set yet.
// methods does not have code information yet.
private class CollectRootVisitor extends ASTVisitor {
private TestClassTable rootClassTable;
private TestFuncTable rootMethodTable;
// old value in rootClassTable is not replaced.
// old value in rootMethodTable is replaced.
public CollectRootVisitor(
TestClassTable rootClassTable, TestFuncTable rootMethodTable) {
this.rootClassTable = rootClassTable;
this.rootMethodTable = rootMethodTable;
}
@Override
public boolean visit(MethodDeclaration node) {
IMethodBinding methodBinding = node.resolveBinding();
if (!AdapterContainer.globalInstance().isRootFunction(methodBinding)) {
return super.visit(node);
}
ITypeBinding classBinding = methodBinding.getDeclaringClass();
if (!classBinding.isClass()) {
throw new RuntimeException("not supported yet: " + classBinding);
}
TestClass rootClass = rootClassTable.getByKey(classBinding.getKey());
if (rootClass == null) {
Pair pair = getTestDoc(classBinding);
if (pair.getRight()) {
rootClass = new PageClass(); // though root class cannot be page class
} else {
rootClass = new TestClass();
}
rootClass.setKey(classBinding.getKey());
rootClass.setQualifiedName(classBinding.getQualifiedName());
rootClass.setTestDoc(pair.getLeft());
rootClassTable.addTestClass(rootClass);
}
TestMethod testMethod = new TestMethod();
testMethod.setKey(methodBinding.getKey());
testMethod.setQualifiedName(ASTUtils.qualifiedMethodName(methodBinding));
Pair pair = getTestDoc(methodBinding);
if (pair != null) {
// pair is null if the root method does not have TestDoc annotation
testMethod.setTestDoc(pair.getLeft());
testMethod.setCaptureStyle(pair.getRight());
}
for (Object element : node.parameters()) {
if (!(element instanceof SingleVariableDeclaration)) {
throw new RuntimeException("not supported yet: " + element);
}
SingleVariableDeclaration varDecl = (SingleVariableDeclaration)element;
testMethod.addArgVariable(varDecl.getName().getIdentifier());
}
testMethod.setTestClassKey(rootClass.getKey());
testMethod.setTestClass(rootClass);
rootMethodTable.addTestFunction(testMethod);
rootClass.addTestMethodKey(testMethod.getKey());
rootClass.addTestMethod(testMethod);
return super.visit(node);
}
}
private class CollectRootRequestor extends FileASTRequestor {
private TestClassTable rootClassTable;
private TestFuncTable rootMethodTable;
public CollectRootRequestor() {
rootClassTable = new TestClassTable();
rootMethodTable = new TestFuncTable();
}
public TestClassTable getRootClassTable() {
return rootClassTable;
}
public TestFuncTable getRootMethodTable() {
return rootMethodTable;
}
@Override
public void acceptAST(String sourceFilePath, CompilationUnit ast) {
ast.accept(new CollectRootVisitor(rootClassTable, rootMethodTable));
}
}
private class CollectSubVisitor extends ASTVisitor {
private TestClassTable subClassTable;
private TestFuncTable subMethodTable;
private TestClassTable rootClassTable;
// rootClassTable if only for read, not write any data.
// old value in subClassTable is not replaced.
// old value in subMethodTable is replaced.
public CollectSubVisitor(TestClassTable rootClassTable,
TestClassTable subClassTable, TestFuncTable subMethodTable) {
this.rootClassTable = rootClassTable;
this.subClassTable = subClassTable;
this.subMethodTable = subMethodTable;
}
@Override
public boolean visit(MethodDeclaration node) {
IMethodBinding methodBinding = node.resolveBinding();
if (!isSubFunction(methodBinding)) {
return super.visit(node);
}
ITypeBinding classBinding = methodBinding.getDeclaringClass();
if (!classBinding.isClass()) {
throw new RuntimeException("not supported yet: " + classBinding);
}
TestClass testClass = rootClassTable.getByKey(classBinding.getKey());
if (testClass == null) {
testClass = subClassTable.getByKey(classBinding.getKey());
if (testClass == null) {
Pair pair = getTestDoc(classBinding);
if (pair.getRight()) {
testClass = new PageClass();
} else {
testClass = new TestClass();
}
testClass.setKey(classBinding.getKey());
testClass.setQualifiedName(classBinding.getQualifiedName());
testClass.setTestDoc(pair.getLeft());
subClassTable.addTestClass(testClass);
}
}
TestMethod testMethod = new TestMethod();
testMethod.setKey(methodBinding.getKey());
testMethod.setQualifiedName(ASTUtils.qualifiedMethodName(methodBinding));
Pair pair = getTestDoc(methodBinding);
testMethod.setTestDoc(pair.getLeft());
testMethod.setCaptureStyle(pair.getRight());
for (Object element : node.parameters()) {
if (!(element instanceof SingleVariableDeclaration)) {
throw new RuntimeException("not supported yet: " + element);
}
SingleVariableDeclaration varDecl = (SingleVariableDeclaration)element;
testMethod.addArgVariable(varDecl.getName().getIdentifier());
}
testMethod.setTestClassKey(testClass.getKey());
testMethod.setTestClass(testClass);
subMethodTable.addTestFunction(testMethod);
testClass.addTestMethodKey(testMethod.getKey());
testClass.addTestMethod(testMethod);
return super.visit(node);
}
}
private class CollectSubRequestor extends FileASTRequestor {
private TestClassTable subClassTable;
private TestFuncTable subMethodTable;
private TestClassTable rootClassTable;
public CollectSubRequestor(TestClassTable rootClassTable) {
this.rootClassTable = rootClassTable;
subClassTable = new TestClassTable();
subMethodTable = new TestFuncTable();
}
public TestClassTable getSubClassTable() {
return subClassTable;
}
public TestFuncTable getSubMethodTable() {
return subMethodTable;
}
@Override
public void acceptAST(String sourceFilePath, CompilationUnit ast) {
ast.accept(new CollectSubVisitor(
rootClassTable, subClassTable, subMethodTable));
}
}
private class CollectCodeVisitor extends ASTVisitor {
private TestFuncTable rootMethodTable;
private TestFuncTable subMethodTable;
private CompilationUnit compilationUnit;
// set code information to method table
public CollectCodeVisitor(TestFuncTable rootMethodTable, TestFuncTable subMethodTable,
CompilationUnit compilationUnit) {
this.rootMethodTable = rootMethodTable;
this.subMethodTable = subMethodTable;
this.compilationUnit = compilationUnit;
}
private Code methodBindingCode(IMethodBinding binding,
Expression thisInstance, List> arguments, String original, TestFunction parentFunc) {
if (binding == null) {
UnknownCode unknownCode = new UnknownCode();
unknownCode.setOriginal(original);
return unknownCode;
}
String uniqueKey = binding.getKey();
TestFunction invocationFunc = subMethodTable.getByKey(uniqueKey);
if (invocationFunc == null) {
// TODO using additionalTestDocKey is temporal logic..
// TODO What does binding.getName method return for constructor method??
String additionalUniqueKey = AdditionalTestDocsSetter.additionalTestDocKey(
binding.getDeclaringClass().getQualifiedName() + "." + binding.getName());
invocationFunc = subMethodTable.getByKey(additionalUniqueKey);
if (invocationFunc == null) {
UnknownCode unknownCode = new UnknownCode();
unknownCode.setOriginal(original);
return unknownCode;
}
}
SubMethodInvoke subMethodInvoke = new SubMethodInvoke();
subMethodInvoke.setSubFunctionKey(invocationFunc.getKey());
subMethodInvoke.setSubFunction(invocationFunc);
if (thisInstance == null) {
subMethodInvoke.setThisInstance(null);
} else {
subMethodInvoke.setThisInstance(expressionCode(thisInstance, parentFunc));
}
for (Object arg : arguments) {
Expression exp = (Expression) arg;
subMethodInvoke.addArg(expressionCode(exp, parentFunc));
}
subMethodInvoke.setOriginal(original);
return subMethodInvoke;
}
private Code generateParamVarCode(Expression expression,
IVariableBinding paramVarBinding, TestFunction parentFunc) {
int argIndex;
if (parentFunc == null) {
argIndex = -1;
} else {
String varName = paramVarBinding.getName();
argIndex = parentFunc.getArgVariables().indexOf(varName);
}
if (argIndex == -1) {
// when fails to resolve parameter variable
return generateUnknownCode(expression);
}
FuncArgument funcArg = new FuncArgument();
funcArg.setOriginal(expression.toString().trim());
funcArg.setArgIndex(argIndex);
return funcArg;
}
private UnknownCode generateUnknownCode(Expression expression) {
UnknownCode unknownCode = new UnknownCode();
unknownCode.setOriginal(expression.toString().trim());
return unknownCode;
}
private Code expressionCode(Expression expression, TestFunction parentFunc) {
if (expression == null) {
StringCode strCode = new StringCode();
strCode.setValue(null);
strCode.setOriginal("null");
return strCode;
} else if (expression instanceof StringLiteral) {
StringCode strCode = new StringCode();
strCode.setValue(((StringLiteral) expression).getLiteralValue());
strCode.setOriginal(expression.toString().trim());
return strCode;
} else if (expression instanceof Assignment) {
Assignment assignment = (Assignment) expression;
return expressionCode(assignment.getRightHandSide(), parentFunc);
} else if (expression instanceof MethodInvocation) {
MethodInvocation invocation = (MethodInvocation) expression;
IMethodBinding binding = invocation.resolveMethodBinding();
return methodBindingCode(binding, invocation.getExpression(),
invocation.arguments(), expression.toString().trim(), parentFunc);
} else if (expression instanceof ClassInstanceCreation) {
ClassInstanceCreation creation = (ClassInstanceCreation) expression;
IMethodBinding binding = creation.resolveConstructorBinding();
return methodBindingCode(binding, null, creation.arguments(),
expression.toString().trim(), parentFunc);
} else if (expression instanceof SimpleName) {
SimpleName simpleName = (SimpleName) expression;
IBinding binding = simpleName.resolveBinding();
if (binding instanceof IVariableBinding) {
IVariableBinding varBinding = (IVariableBinding) binding;
if (varBinding.isParameter()) {
return generateParamVarCode(expression, varBinding, parentFunc);
} else {
return generateUnknownCode(expression);
}
} else {
return generateUnknownCode(expression);
}
} else{
return generateUnknownCode(expression);
}
}
@Override
public boolean visit(MethodDeclaration node) {
TestFunction testFunc;
IMethodBinding methodBinding = node.resolveBinding();
if (AdapterContainer.globalInstance().isRootFunction(methodBinding)) {
testFunc = rootMethodTable.getByKey(methodBinding.getKey());
} else if (isSubFunction(methodBinding)) {
testFunc = subMethodTable.getByKey(methodBinding.getKey());
} else {
return super.visit(node);
}
List> list = node.getBody().statements();
for (Object obj : list) {
assert obj instanceof ASTNode;
ASTNode statementNode = (ASTNode) obj;
Code code;
if (statementNode instanceof ExpressionStatement) {
Expression expression = ((ExpressionStatement)statementNode).getExpression();
code = expressionCode(expression, testFunc);
} else if (statementNode instanceof VariableDeclarationStatement) {
// TODO assume single VariableDeclarationFragment
VariableDeclarationFragment varFrag
= (VariableDeclarationFragment)(((VariableDeclarationStatement)statementNode).fragments().get(0));
Expression expression = varFrag.getInitializer();
code = expressionCode(expression, testFunc);
} else {
code = new UnknownCode();
code.setOriginal(statementNode.toString().trim());
}
CodeLine codeLine = new CodeLine();
codeLine.setStartLine(compilationUnit.getLineNumber(statementNode.getStartPosition()));
codeLine.setEndLine(compilationUnit.getLineNumber(
statementNode.getStartPosition() + statementNode.getLength()));
codeLine.setCode(code);
// sometimes original value set by expressionCode method does not equal to the on of statementNode
code.setOriginal(statementNode.toString().trim());
testFunc.addCodeBody(codeLine);
}
return super.visit(node);
}
}
private class CollectCodeRequestor extends FileASTRequestor {
private TestFuncTable rootMethodTable;
private TestFuncTable subMethodTable;
public CollectCodeRequestor(TestFuncTable rootMethodTable, TestFuncTable subMethodTable) {
this.rootMethodTable = rootMethodTable;
this.subMethodTable = subMethodTable;
}
@Override
public void acceptAST(String sourceFilePath, CompilationUnit ast) {
ast.accept(new CollectCodeVisitor(rootMethodTable, subMethodTable, ast));
}
}
// srcFiles..parse target files
// srcEncoding.. encoding of srcFiles. "UTF-8" etc
// classPathEntries.. all class paths (class file containing directory or jar file) srcFiles depend.
// this path value is similar to --classpath command line argument, but you must give
// all class containing sub directories even if the class is in a named package
public SrcTree generate(String[] srcFiles, String srcEncoding, String[] classPathEntries) {
// collect root class and method table without code body
CollectRootRequestor rootRequestor = new CollectRootRequestor();
parseAST(srcFiles, srcEncoding, classPathEntries, rootRequestor);
// collect sub class and method table without code body
CollectSubRequestor subRequestor = new CollectSubRequestor(rootRequestor.getRootClassTable());
parseAST(srcFiles, srcEncoding, classPathEntries, subRequestor);
// add not used additional TestDoc to the table
AdditionalTestDocsSetter setter = new AdditionalTestDocsSetter(
rootRequestor.getRootClassTable(), subRequestor.getSubClassTable(),
rootRequestor.getRootMethodTable(), subRequestor.getSubMethodTable());
setter.set(additionalTestDocs);
// collect code
CollectCodeRequestor codeRequestor = new CollectCodeRequestor(
rootRequestor.getRootMethodTable(), subRequestor.getSubMethodTable());
parseAST(srcFiles, srcEncoding, classPathEntries, codeRequestor);
SrcTree result = new SrcTree();
result.setRootClassTable(rootRequestor.getRootClassTable());
result.setSubClassTable(subRequestor.getSubClassTable());
result.setRootFuncTable(rootRequestor.getRootMethodTable());
result.setSubFuncTable(subRequestor.getSubMethodTable());
return result;
}
private void addToClassPathListFromJarManifest(List classPathList, File jarFile) {
if (!jarFile.exists()) {
return; // do nothing
}
Manifest manifest = CommonUtils.readManifestFromExternalJar(jarFile);
// jar class path is sometimes not set at java.class.path property
// (this case happens for Maven surefire plug-in.
// see http://maven.apache.org/surefire/maven-surefire-plugin/examples/class-loading.html)
String jarClassPathStr = manifest.getMainAttributes().getValue("Class-Path");
if (jarClassPathStr != null) {
String[] jarClassPathArray = jarClassPathStr.split(" "); // separator is space character
addToClassPathList(classPathList, jarClassPathArray);
}
}
private void addToClassPathList(List classPathList, String[] classPathArray) {
for (String classPath : classPathArray) {
if (classPath == null || classPath.trim().equals("")) {
continue;
}
String classPathWithoutPrefix;
if (classPath.startsWith("file:")) {
// class path in the jar MANIFEST sometimes has this form of class path
classPathWithoutPrefix = classPath.substring(5);
} else {
classPathWithoutPrefix = classPath;
}
String absClassPath = new File(classPathWithoutPrefix).getAbsolutePath();
if (absClassPath.endsWith(".jar")) {
if (!classPathList.contains(absClassPath)) {
classPathList.add(absClassPath);
addToClassPathListFromJarManifest(classPathList, new File(absClassPath));
}
} else if (absClassPath.endsWith(".zip")) {
if (!classPathList.contains(absClassPath)) {
classPathList.add(absClassPath);
}
} else {
File classPathFile = new File(absClassPath);
if (classPathFile.isDirectory()) {
// needs to add all sub directories
// since SrcTreeGenerator does not search classPathEntry sub directories
// TODO should add jar file in the sub directories ??
Collection subDirCollection = FileUtils.listFilesAndDirs(
classPathFile, FileFilterUtils.directoryFileFilter(), FileFilterUtils.trueFileFilter());
for (File subDir : subDirCollection) {
if (!classPathList.contains(subDir.getAbsolutePath())) {
classPathList.add(subDir.getAbsolutePath());
}
}
}
}
}
}
public SrcTree generateWithRuntimeClassPath(File srcRootDir, String srcEncoding)
throws IllegalTestScriptException {
// set up srcFilePaths
if (!srcRootDir.exists()) {
throw new IllegalArgumentException("directory does not exist: " + srcRootDir.getAbsolutePath());
}
Collection srcFileCollection = FileUtils.listFiles(srcRootDir, new String[]{"java"}, true);
List srcFileList = new ArrayList(srcFileCollection);
String[] srcFilePaths = new String[srcFileList.size()];
for (int i = 0; i < srcFileList.size(); i++) {
srcFilePaths[i] = srcFileList.get(i).getAbsolutePath();
}
// set up classPathEntries
// TODO handle wild card classpath entry
List classPathList = new ArrayList(64);
String classPathStr = System.getProperty("java.class.path");
String[] classPathArray = classPathStr.split(Pattern.quote(File.pathSeparator));
addToClassPathList(classPathList, classPathArray);
for (String classPath : classPathList) {
logger.info("classPath: " + classPath);
}
return generate(srcFilePaths, srcEncoding, classPathList.toArray(new String[0]));
}
}