com.sap.psr.vulas.monitor.trace.StackTraceUtil Maven / Gradle / Ivy
/**
* This file is part of Eclipse Steady.
*
* Licensed 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.
*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved.
*/
package com.sap.psr.vulas.monitor.trace;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.sap.psr.vulas.ConstructId;
import com.sap.psr.vulas.java.JavaClassId;
import com.sap.psr.vulas.java.JavaConstructorId;
import com.sap.psr.vulas.java.JavaId;
import com.sap.psr.vulas.java.JavaMethodId;
import com.sap.psr.vulas.monitor.ClassVisitor;
import com.sap.psr.vulas.monitor.Loader;
import com.sap.psr.vulas.monitor.LoaderHierarchy;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.NotFoundException;
/**
* Transforms a stacktrace, obtained from {@link Throwable} as an array of {@link StackTraceElement},
* into a linked list of corresponding construct identifiers.
*
* Analyzes the bytecode in order to find methods in case of method overloading
* (thereby using the line numbers provided in the stack trace).
*
* More information can be found here:
* http://stackoverflow.com/questions/421280/how-do-i-find-the-caller-of-a-method-using-stacktrace-or-reflection
* http://stackoverflow.com/questions/15899518/modifying-line-numbers-in-javassist
* http://stackoverflow.com/questions/1069066/get-current-stack-trace-in-java
*/
public class StackTraceUtil {
private static Log log = null;
private static final Log getLog() {
if(StackTraceUtil.log==null)
StackTraceUtil.log = LogFactory.getLog(StackTraceUtil.class);
return StackTraceUtil.log;
}
private Loader loader = null;
private boolean stopAtJUnit = false;
private static final String ANNO_JUNIT_TEST = "org.junit.Test";
/** Remember ClassNotFoundExceptions in order to not print them again and again. */
static private final Set cnfe = new HashSet();
/** Remember ConstructIds built from stack trace elements. */
static private final Map constructIds = new HashMap();
/**
* Constructor for StackTraceUtil.
*/
public StackTraceUtil() {}
/**
* Constructor for StackTraceUtil.
*
* @param _hier a {@link com.sap.psr.vulas.monitor.LoaderHierarchy} object.
* @param _loader a {@link com.sap.psr.vulas.monitor.Loader} object.
*/
public StackTraceUtil(LoaderHierarchy _hier, Loader _loader) {
//_hier.logHierarchy(_hier.getRoot(), 0);
this.loader = _loader;
}
/**
* Setter for the field stopAtJUnit
.
*
* @param _b a boolean.
*/
public void setStopAtJUnit(boolean _b) { this.stopAtJUnit = _b; }
/**
* Returns the JUnit test case representing the starting point of a previously computed path.
* If no JUnit test case is present, the method returns null.
*
* @param _path a {@link java.util.List} object.
* @return a {@link com.sap.psr.vulas.ConstructId} object.
*/
public ConstructId getJUnitContext(List _path) {
ConstructId junit = null, node = null;
final Iterator iter = _path.iterator();
while(iter.hasNext()) {
node = iter.next().getConstructId();
if(node instanceof JavaId) {
if(((JavaId)node).hasAnnotation(ANNO_JUNIT_TEST)) {
junit = node;
break;
}
}
}
return junit;
}
/**
* Transforms the stacktrace (obtained by, for instance, new Throwable().getStackTrace()) into a linked list of construct identifiers.
*
* @param _st an array of {@link java.lang.StackTraceElement} objects.
* @param _change_list_element the construct ID for which the stack trace was collected (if known by caller, can be null)
* @return a {@link java.util.List} object.
*/
public List transformStackTrace(StackTraceElement[] _st, PathNode _change_list_element) {
// The path to be constructed and returned
final List path = new LinkedList();
// Loop all elements of the stack trace
boolean cont = true;
int i = 0;
while(cont && i<_st.length) {
// Ignore the first if it is method getStackTrace
if(i==0 && _st[i].getClassName().equals("java.lang.Thread") && _st[i].getMethodName().equals("getStackTrace"))
i++;
else {
// If the path is empty, take the change list element (if provided), herewith avoid overloading problems
if(path.isEmpty() && _change_list_element!=null)
path.add(_change_list_element);
// Other elements: Find and add a construct ID for the next stack trace element to
else
cont = this.getConstructId(path, _st[i]);
i++;
}
}
// Debug information
if(StackTraceUtil.getLog().isDebugEnabled()) {
StackTraceUtil.getLog().debug("Stacktrace with [" + _st.length + "] elements transformed into path with [" + path.size() + "] nodes");
final StringBuilder b = new StringBuilder();
int j = 0, k = path.size()-1;
while(j<_st.length) {
b.delete(0, b.length());
b.append(" ").append(_st[j].toString());
if(j==0 && _st[j].getClassName().equals("java.lang.Thread") && _st[j].getMethodName().equals("getStackTrace")) {
j++;
}
else {
j++;
if(k>=0) {
b.append(" --> ").append(path.get(k).toString());
k--;
}
}
StackTraceUtil.getLog().debug(b.toString());
}
}
return path;
}
/**
*
* @param _path
* @param _e
* @param _loader
* @return true if the path construction shall continue
*/
private boolean getConstructId(List _path, StackTraceElement _e) {
// To be returned, determines whether the path construction continues after the current element
boolean cont = true;
// The one or multiple construct IDs found for <_e>
List construct_ids = new ArrayList();
// Classloader
ClassLoader cl = null;
if(this.loader!=null)
cl = this.loader.getClassLoader();
else
cl = this.getClass().getClassLoader();
Class c = null;
JavaMethodId meth_cid = null;
JavaConstructorId cons_cid = null;
try {
// Get the Java class definition, break if that does not work
c = cl.loadClass(_e.getClassName());
// To be built from the
String qname = null;
// Constructor
if(_e.getMethodName().equals("")) {
for(Constructor con: c.getDeclaredConstructors()) {
// Cleanup the constructor's string representation for parsing
qname = con.toString();
qname = qname.substring(0, qname.lastIndexOf(")")+1);
if(qname.indexOf(" ")!=-1) qname = qname.substring(qname.lastIndexOf(" ")+1);
qname = ClassVisitor.removeParameterQualification(qname);
try {
cons_cid = JavaId.parseConstructorQName(qname);
for(Annotation a: con.getAnnotations()) cons_cid.addAnnotation(a.annotationType().getName());
construct_ids.add(cons_cid);
} catch (Exception e) {
StackTraceUtil.getLog().error("Exception while creating construct ID: " + e.getMessage());
}
}
// There's some likelihood that multiple constructors are defined
// If we have line number info, use Javassist to find the right one
if(construct_ids.size()>1) {
if(_e.getLineNumber()<=0)
StackTraceUtil.getLog().warn("Stack trace element w/o line number info, cannot use Javassist: " + _e);
else {
try {
final ConstructId the_one = this.filterConstructors(_e, construct_ids);
if(the_one!=null) {
construct_ids.clear();
construct_ids.add(the_one);
}
else {
StackTraceUtil.getLog().error("Could not determine constructor despite line information: [" + _e.toString() + "]");
}
} catch (NotFoundException e) {
StackTraceUtil.getLog().error("Javassist class not found exception for class [" + e.getMessage() + "], classloader [" + cl.getClass().toString() + "]");
}
}
}
}
// Static initializer
else if(_e.getMethodName().equals("")) {
final JavaClassId jcid = JavaId.getClassId(c);
construct_ids.add(jcid.getClassInit());
}
// Method
else {
for(Method m: c.getDeclaredMethods()) {
if(m.getName().equals(_e.getMethodName())) {
// Cleanup the method's string representation for parsing
qname = m.toString();
qname = qname.substring(0, qname.lastIndexOf(")")+1);
if(qname.indexOf(" ")!=-1) qname = qname.substring(qname.lastIndexOf(" ")+1);
qname = ClassVisitor.removeParameterQualification(qname);
try {
meth_cid = JavaId.parseMethodQName(qname);
for(Annotation a: m.getAnnotations()) meth_cid.addAnnotation(a.annotationType().getName());
construct_ids.add(meth_cid);
} catch (Exception e) {
StackTraceUtil.getLog().error("Exception while creating construct ID: " + e.getMessage());
}
}
}
// If the method is overloaded, multiple methods with the same name are found
// If we have line number info, use Javassist to find the right one
if(construct_ids.size()>1) {
// If the path is empty, we can take
if(_e.getLineNumber()<=0)
StackTraceUtil.getLog().warn("Stack trace element w/o line number info, cannot use Javassist: " + _e);
else {
try {
final ConstructId the_one = this.filterMethods(_e, construct_ids);
if(the_one!=null) {
construct_ids.clear();
construct_ids.add(the_one);
}
else {
StackTraceUtil.getLog().error("Could not determine method despite line information: [" + _e.toString() + "]");
}
} catch (NotFoundException e) {
StackTraceUtil.getLog().error("Javassist class not found exception for class [" + e.getMessage() + "], classloader [" + cl.getClass().toString() + "]");
}
}
}
}
if(construct_ids.size()==0) {
StackTraceUtil.getLog().warn("Class [" + _e.getClassName() + "] has no method with name [" + _e.getMethodName() + "]: Stop path construction");
cont = false;
}
else if(construct_ids.size()==1) {
//StackTraceUtil.getLog().info(" Class [" + ste.getClassName() + "] has 1 method with name [" + ste.getMethodName() + "]");
_path.add(0, new PathNode(construct_ids.get(0)));
cont = this.continueStacktraceTransformation((JavaId)construct_ids.get(0));
}
else if(construct_ids.size()>1) {
StackTraceUtil.getLog().warn("Class [" + _e.getClassName() + "] has " + construct_ids.size() + " constructs with name [" + _e.getMethodName() + "]: Take first");
_path.add(0, new PathNode(construct_ids.get(0)));
cont = this.continueStacktraceTransformation((JavaId)construct_ids.get(0));
}
} catch (ClassNotFoundException e) {
if(!StackTraceUtil.cnfe.contains(e.getMessage())) {
StackTraceUtil.getLog().warn("Java class not found exception for class [" + e.getMessage() + "], classloader [" + cl.getClass().toString() + "]: Stop path construction");
StackTraceUtil.cnfe.add(e.getMessage());
}
cont = false;
} catch (SecurityException e) {
StackTraceUtil.getLog().warn("Security exception while analyzing class [" + e.getMessage() + "]: Stop path construction");
cont = false;
} catch (NoClassDefFoundError ncdfe) {
StackTraceUtil.getLog().warn("No class definition exception for class [" + ncdfe.getMessage() + "]: Stop path construction");
cont = false;
}
return cont;
}
/**
* Returns true if the given JavaId has the annotation "org.junit.Test" and the stack trace util has been configured to stop at JUnit tests, false otherwise.
* @param _jid the JavaId whose annotations are checked
* @return
*/
private boolean continueStacktraceTransformation(JavaId _jid) {
boolean cont = true;
if(_jid==null)
cont = false;
else if(_jid.hasAnnotation(ANNO_JUNIT_TEST)) {
if(this.stopAtJUnit) {
StackTraceUtil.getLog().debug("Found JUnit test [" + _jid.getQualifiedName() + "]: Stop path construction");
cont = false;
}
else
StackTraceUtil.getLog().debug("Found JUnit test [" + _jid.getQualifiedName() + "]");
}
return cont;
}
private ConstructId filterMethods(StackTraceElement _e, List _methods) throws NotFoundException {
// To be returned
ConstructId method_found = null;
final String class_qname = ((JavaMethodId)_methods.get(0)).getDefinitionContext().getQualifiedName();
// Get the CtClass
ClassPool cp = null;
CtClass ctclass = null;
if(this.loader==null) {
cp = ClassPool.getDefault();
ctclass = cp.get(class_qname);
}
else {
boolean search = true;
Loader l = this.loader;
NotFoundException nfe = null;
while(search) {
try {
cp = l.getClassPool();
ctclass = cp.get(class_qname);
search = false;
} catch (NotFoundException e) {
StackTraceUtil.getLog().error("Class [" + class_qname + "] not found with class loader [" + l + "]:" + e.getMessage());
if(!l.isRoot())
l = l.getParent();
else {
search = false;
throw e;
}
}
}
}
// Loop all methods of the class having the same name
int shortest_distance = Integer.MAX_VALUE;
int current_distance = -1;
for(ConstructId m: _methods) {
// Loop all methods of the class and find the one closest to the line number of the stack trace element
for(CtMethod ctm: ctclass.getDeclaredMethods()) {
if(m.getQualifiedName().equals(ClassVisitor.removeParameterQualification(ctm.getLongName())) && _e.getLineNumber()>=ctm.getMethodInfo().getLineNumber(0)) {
current_distance = _e.getLineNumber() - ctm.getMethodInfo().getLineNumber(0);
if(current_distance < shortest_distance) {
shortest_distance = current_distance;
method_found = m;
}
}
}
}
return method_found;
}
private ConstructId filterConstructors(StackTraceElement _e, List _constructors) throws NotFoundException {
// To be returned
ConstructId constructor_found = null;
final SortedMap constructor_line_numbers = new TreeMap();
// Get the CtClass
ClassPool cp = null;
if(this.loader!=null)
cp = this.loader.getClassPool();
else
cp = ClassPool.getDefault();
final CtClass ctclass = cp.get( ((JavaConstructorId)_constructors.get(0)).getDefinitionContext().getQualifiedName() );
// Loop all constructors of the class and find the one closest to the line number of the stack trace element
int shortest_distance = Integer.MAX_VALUE;
int current_distance = -1;
ConstructId c = null;
for(CtConstructor ctm: ctclass.getDeclaredConstructors()) {
c = JavaId.parseConstructorQName(ClassVisitor.removeParameterQualification(ctm.getLongName()));
constructor_line_numbers.put(new Integer(ctm.getMethodInfo().getLineNumber(0)), c);
if(_e.getLineNumber()>=ctm.getMethodInfo().getLineNumber(0)) {
current_distance = _e.getLineNumber() - ctm.getMethodInfo().getLineNumber(0);
if(current_distance < shortest_distance) {
shortest_distance = current_distance;
constructor_found = c;
}
}
}
// Print if no constructor was found (wrong line number in stacktrace?)
// 2 examples from commons-compress 1.4:
//[main] WARN com.sap.psr.vulas.monitor.StackTraceUtil - None of the constructors at line numbers [132,144,159] matched the stack trace element [org.apache.commons.compress.archivers.zip.ZipArchiveInputStream.(ZipArchiveInputStream.java:87)], return constructor with smallest line number
//[main] WARN com.sap.psr.vulas.monitor.StackTraceUtil - None of the constructors at line numbers [97,111,136,145,158] matched the stack trace element [org.apache.commons.compress.archivers.zip.ZipArchiveEntry.(ZipArchiveEntry.java:86)], return constructor with smallest line number
if(constructor_found==null && !constructor_line_numbers.isEmpty()) {
constructor_found = constructor_line_numbers.get(constructor_line_numbers.keySet().iterator().next());
StackTraceUtil.getLog().debug("None of the constructors at line numbers [" + StringUtils.join(constructor_line_numbers.keySet(), ',') + "] matched the stack trace element [" + _e + "], return constructor with smallest line number");
}
return constructor_found;
}
/**
* Given a stacktrace object return the predecessor of the element that called this stacktrace.
*
* @param _st an array of {@link java.lang.StackTraceElement} objects.
* @return a {@link com.sap.psr.vulas.ConstructId} object.
*/
public ConstructId getPredecessorConstruct(StackTraceElement[] _st) {
// StackTraceUtil.constructIds can be null if the method is called a second time during instantiation
if(_st.length > 2 && StackTraceUtil.constructIds!=null) {
// We already searched in the past
if(StackTraceUtil.constructIds.containsKey(_st[1])) {
return StackTraceUtil.constructIds.get(_st[1]); // Can be null
}
// We did not search before, search now
else {
final List path = new LinkedList();
this.getConstructId(path, _st[1]);
// We were able to create a constructId for the predecessor
if(path.size()>0) {
final ConstructId cid = path.get(0).getConstructId();
StackTraceUtil.constructIds.put(_st[1], cid);
return cid;
}
// We failed, e.g., because the class could not be found
else {
StackTraceUtil.constructIds.put(_st[1], null);
//StackTraceUtil.getLog().error("Could not create construct from stack trace element [" + _st[1] + "], which is the predecessor of [" + _st[0] + "]");
return null;
}
}
}
return null;
}
}