com.sap.psr.vulas.monitor.ClassVisitor 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;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.sap.psr.vulas.ConstructId;
import com.sap.psr.vulas.core.util.CoreConfiguration;
import com.sap.psr.vulas.java.JavaClassId;
import com.sap.psr.vulas.java.JavaId;
import com.sap.psr.vulas.java.JavaId.Type;
import com.sap.psr.vulas.shared.json.model.Application;
import com.sap.psr.vulas.shared.util.FileUtil;
import com.sap.psr.vulas.shared.util.VulasConfiguration;
import javassist.CannotCompileException;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.ConstPool;
import javassist.bytecode.annotation.Annotation;
/**
* Identifies all methods and constructors in a given Java class (using Javassist).
* Can inject instrumentation code into constructors and methods in order to collect
* trace information during application tests.
*/
public class ClassVisitor {
// ====================================== STATIC MEMBERS
private static Log log = null;
private static final Log getLog() {
if(ClassVisitor.log==null)
ClassVisitor.log = LogFactory.getLog(ClassVisitor.class);
return ClassVisitor.log;
}
// ====================================== INSTANCE MEMBERS
private JavaId javaId = null;
private String qname = null;
private CtClass c = null;
/** The outer class of nested classes. */
private CtClass declaringClass = null;
/** True if the class is already instrumented. */
private Boolean isInstrumented = null;
private String originalArchiveDigest = null;
private Application appContext = null;
private byte[] bytes = null;
/** Major version of the class file format (see https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html). */
private int major = 0;
/** Minor version of the class file format (see https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html). */
private int minor = 0;
/** The constructs found in the given class. */
private Set constructs = null;
private boolean writeCodeToTmp = false;
private String[] fieldAnnotations = null;
/**
* Constructor for ClassVisitor.
*
* @param _c a {@link javassist.CtClass} object.
*/
public ClassVisitor(CtClass _c) {
// Build the JavaId
if(_c.isInterface())
throw new IllegalArgumentException("[" + _c.getName() + "]: Interfaces are not supported");
else if(_c.isEnum())
this.javaId = JavaId.parseEnumQName(_c.getName());
else
this.javaId = JavaId.parseClassQName(_c.getName());
this.qname = this.javaId.getQualifiedName();
this.c = _c;
// Remember major/minor
final ClassFile cf = _c.getClassFile();
this.major = cf.getMajorVersion();
this.minor = cf.getMinorVersion();
// For nested classes, get the declaring (outer) class: It is used to skip the first argument in non-static inner classes
try {
this.declaringClass = _c.getDeclaringClass();
} catch (NotFoundException e) {
// Only a problem in case of non-static inner classes, because in that case the 1st argument of the constructor cannot be removed, cf. method visitConstructors(boolean)
if(!Modifier.isStatic(this.c.getModifiers()))
ClassVisitor.getLog().warn("No declaring class found for non-static inner class [" + this.javaId.getQualifiedName() + "]");//: " + e.getMessage());
}
this.writeCodeToTmp = VulasConfiguration.getGlobal().getConfiguration().getBoolean(CoreConfiguration.INSTR_WRITE_CODE, false);
this.fieldAnnotations = VulasConfiguration.getGlobal().getStringArray(CoreConfiguration.INSTR_FLD_ANNOS, new String[] {});
}
/**
* Returns a set with the {@link ConstructId}s of all constructs contained in the given Java class, i.e.,
* all methods, all constructors, the class or enumeration and the package (unless it is the default package,
* i.e., no package).
*
* @return a {@link java.util.Set} object.
*/
public Set getConstructs() {
if(this.constructs==null) {
this.constructs = new TreeSet();
this.constructs.add(this.javaId);
// Do not add the default package (qualified name=="")
// This makes the constructs obtained from java and class files more comparable
if(!this.javaId.getJavaPackageId().getSimpleName().equals(""))
this.constructs.add(this.javaId.getJavaPackageId());
try {
this.constructs.addAll(this.visitConstructors(false));
this.constructs.addAll(this.visitMethods(false));
} catch (CannotCompileException e) {
// Should never happen since we do not instrument in this case (argument is false)
}
}
return this.constructs;
}
/**
* isInstrumented.
*
* @return a boolean.
*/
public synchronized boolean isInstrumented() {
if(this.isInstrumented==null) {
try {
this.c.getDeclaredField("VUL_CLS_INS");
this.isInstrumented = new Boolean(true);
}
catch(NotFoundException e) {
this.isInstrumented = new Boolean(false);
}
}
return this.isInstrumented.booleanValue();
}
/**
* visitMethods.
*
* @param _instrument a boolean.
* @return a {@link java.util.Set} object.
* @throws javassist.CannotCompileException if any.
*/
public synchronized Set visitMethods(boolean _instrument) throws CannotCompileException {
final Set constructs = new HashSet();
final CtMethod[] methods = this.c.getDeclaredMethods();
// Loop all methods
JavaId jid = null;
for(CtMethod meth : methods) {
jid = JavaId.parseMethodQName(this.javaId.getType(), ClassVisitor.removeParameterQualification(meth.getLongName()));
constructs.add(jid);
// Instrument if requested, not yet done and possible (= not empty, as for abstract methods)
final boolean is_native = Modifier.isNative(meth.getModifiers());
if(_instrument && !isInstrumented() && !meth.isEmpty() && !is_native && meth.getLongName().startsWith(this.qname)) {
try {
this.instrument(jid, meth);
}
// Can happen if dependencies of the class are not available in the ClassPool
catch (CannotCompileException ex) {
//ClassVisitor.getLog().info("Exception while instrumenting " + jid + ": " + ex.getMessage());
throw ex;
}
}
}
ClassVisitor.getLog().debug("Class '" + this.qname + "': " + methods.length + " methods");
return constructs;
}
/**
* visitConstructors.
*
* @param _instrument a boolean.
* @return a {@link java.util.Set} object.
* @throws javassist.CannotCompileException if any.
*/
public synchronized Set visitConstructors(boolean _instrument) throws CannotCompileException {
final Set constructs = new HashSet();
final CtConstructor[] constructors = this.c.getDeclaredConstructors();
String constr_name = null;
JavaId jid = null;
// Skip the first constructor parameter (added by the compiler for non-static classes)?
final String param_to_skip = ( this.declaringClass!=null && !Modifier.isStatic(this.c.getModifiers()) ? ClassVisitor.removePackageContext(this.declaringClass.getName()) : null );
// Static initializer exists: Add it to the constructs and instrument (if requested)
final CtConstructor initializer = this.c.getClassInitializer();
if(initializer != null && this.javaId.getType()==Type.CLASS) {
jid = ((JavaClassId)this.javaId).getClassInit();
constructs.add(jid);
// Instrument if requested
if(_instrument && !isInstrumented() && !initializer.isEmpty() && initializer.getLongName().startsWith(this.qname)) {
try {
constr_name = this.removeParameterQualification(initializer.getLongName());
this.instrument(jid, initializer);
}
// Can happen if dependencies of the class are not available in the ClassPool
catch (CannotCompileException ex) {
//ClassVisitor.getLog().error("Exception while instrumenting initializer [" + constr_name + "]: " + ex.getMessage());
throw ex;
}
}
}
// Loop all constructors and instrument (if requested)
for(CtConstructor constr : constructors) {
jid = JavaId.parseConstructorQName(this.javaId.getType(), ClassVisitor.removeParameterQualification(constr.getLongName()), param_to_skip);
constructs.add(jid);
// Instrument if requested
if(_instrument && !isInstrumented() && !constr.isEmpty() && constr.getLongName().startsWith(this.qname)) {
try {
constr_name = this.removeParameterQualification(constr.getLongName());
this.instrument(jid, constr);
}
// Can happen if dependencies of the class are not available in the ClassPool
catch (CannotCompileException ex) {
//ClassVisitor.getLog().error("Exception while instrumenting constructor [" + constr_name + "]: " + ex.getMessage());
throw ex;
}
}
}
ClassVisitor.getLog().debug("Class '" + this.qname + "': " + constructors.length + " constructors");
return constructs;
}
private void instrument(JavaId _jid, CtBehavior _behavior) throws CannotCompileException {
// Loop all instrumentors to build the to-be-injected source code
final List instrumentorList = InstrumentorFactory.getInstrumentors();
final Iterator iter = instrumentorList.iterator();
final StringBuffer instrumentation_code = new StringBuffer();
while(iter.hasNext()) {
IInstrumentor i = iter.next();
if(i.acceptToInstrument(_jid, _behavior, this)) {
i.instrument(instrumentation_code, _jid, _behavior, this);
}
}
// Return if there's nothing to inject
if(instrumentation_code.length()==0)
return;
// If there's something to inject, surround it by a try clause
final StringBuffer source_code = new StringBuffer();
source_code.append("try {");
source_code.append(instrumentation_code.toString());
source_code.append("}");
source_code.append("catch(IllegalStateException ise) { throw ise; }");
source_code.append("catch(Throwable e) { System.err.println(e.getClass().getName() + \" occurred during execution of instrumentation code in " + _jid.toString() + ": \" + e.getMessage()); }");
// Remember an exception (if any) and throw it at the end
CannotCompileException cce = null;
// Inject the code
try {
if(_jid.getType().equals(JavaId.Type.CONSTRUCTOR) || _jid.getType().equals(JavaId.Type.CLASSINIT))
_behavior.insertAfter(source_code.toString());
else
_behavior.insertBefore(source_code.toString());
} catch (CannotCompileException e) {
cce = e;
}
// Write source and byte code to tmp
if(source_code.length()>0 && (this.writeCodeToTmp || cce!=null)) {
Path p = null;
try {
p = Paths.get(VulasConfiguration.getGlobal().getTmpDir().toString(), _jid.getJavaPackageId().getQualifiedName().replace('.','/'), _jid.getDefinitionContext().getName() + "." + _jid.getName().replace('<', '_').replace('>','_') + ".java");
FileUtil.createDirectory(p.getParent());
FileUtil.writeToFile(p.toFile(), ClassVisitor.prettyPrint(source_code.toString()));
// Only log the writing of the source code in case of a previous exception
if(cce!=null)
ClassVisitor.getLog().warn("Compile exception when adding code to " + _jid + ": Instrumentation code written to file [" + p + "]");
} catch (IOException e) {
ClassVisitor.getLog().warn("Cannot write instrumentation code of " + _jid + " to file [" + p + "]: " + e.getMessage());
} catch(InvalidPathException ipe) {
ClassVisitor.getLog().warn("Cannot write instrumentation code of " + _jid + " to file [" + p + "]: " + ipe.getMessage());
}
}
// Throw exception (if any)
if(cce!=null)
throw cce;
}
/**
* If called, the given SHA1 digest will be included in the instrumented code (as literal argument in the callback).
* Like this, the digest of the original JAR is preserved even if the instrumented class is written back to the
* archive.
*
* This method must be called prior to the invocation of visitMethods and visitConstructors, as the instrumentation
* code varies depending on whether the digest is known or not.
*
* @param _sha1 a {@link java.lang.String} object.
*/
public synchronized void setOriginalArchiveDigest(String _sha1) { this.originalArchiveDigest = _sha1; }
/**
* If called, the application context (Maven groupId, artifactId and version) will be included in the instrumented code (as literal argument in the callback).
* Like this, the trace collector already knows the application context and does not need to determine it itself.
* *
* This method must be called prior to the invocation of visitMethods and visitConstructors, as the instrumentation
* code varies depending on whether the app context is known or not.
*
* @param _ctx a {@link com.sap.psr.vulas.shared.json.model.Application} object.
*/
public synchronized void setAppContext(Application _ctx) { this.appContext = _ctx; }
/**
* Adds one additional member to the class: A boolean to indicate that the class has been instrumented
* (so that later processes and threads do not need to do it again).
*
* @throws javassist.CannotCompileException if any.
* @throws java.io.IOException if any.
*/
public synchronized void finalizeInstrumentation() throws CannotCompileException, IOException {
// Add member to indicate that the class has been instrumented
if(!this.isInstrumented()) {
this.addBooleanMember("VUL_CLS_INS", true, true);
this.isInstrumented = new Boolean(true);
}
// this.bytes = this.c.toBytecode();
// Alternatively, create the byte array from the classfile
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
final DataOutputStream dos = new DataOutputStream(bos);
final ClassFile cf = c.getClassFile();
// Set major and minor version of the new class file (max. JAVA 7), preferably from the original class file (as read in the constructor)
// Todo: Make max. version configurable
cf.setMajorVersion(Math.min(this.major, ClassFile.JAVA_7));
cf.setMinorVersion(this.minor);
cf.write(dos);
dos.flush();
this.bytes = bos.toByteArray();
this.c.detach(); // Removes the CtClass from the ClassPool
if(this.writeCodeToTmp) {
Path p = null;
try {
p = Paths.get(VulasConfiguration.getGlobal().getTmpDir().toString(), this.javaId.getJavaPackageId().getQualifiedName().replace('.','/'), this.javaId.getName().replace('<', '_').replace('>','_') + ".class");
FileUtil.createDirectory(p.getParent());
FileUtil.writeToFile(p.toFile(), this.bytes);
} catch(IOException e) {
ClassVisitor.getLog().warn("Cannot write bytecode of " + this.javaId+ " to file [" + p + "]: " + e.getMessage());
} catch(InvalidPathException ipe) {
ClassVisitor.getLog().warn("Cannot write bytecode of " + this.javaId+ " to file [" + p + "]: " + ipe.getMessage());
}
}
}
/**
* Returns the byte code of the visited class.
*
* @return the byte code of the visited class
*/
public byte[] getBytecode() { return this.bytes.clone(); }
/**
* addBooleanMember.
*
* @param _field_name a {@link java.lang.String} object.
* @param _value a boolean.
* @param _final a boolean.
* @throws javassist.CannotCompileException if any.
*/
public synchronized void addBooleanMember(String _field_name, boolean _value, boolean _final) throws CannotCompileException {
final CtField f = new CtField(CtClass.booleanType, _field_name, this.c);
if(!_final)
f.setModifiers(Modifier.PUBLIC | Modifier.STATIC | Modifier.TRANSIENT);
else
f.setModifiers(Modifier.PUBLIC | Modifier.STATIC | Modifier.TRANSIENT | Modifier.FINAL);
// Avoid problems with JDO/JPA
this.addFieldAnnotations(f, this.fieldAnnotations);
this.c.addField(f, new Boolean(_value).toString());
}
/**
* Adds the given annotations to the given field. Can be used to avoid problems with OR mappers by adding
* an annotation "javax.persistence.Transient".
* @param _fld
* @param _annotations
*/
private void addFieldAnnotations(CtField _fld, String[] _annotations) {
if(_annotations!=null && _annotations.length>0) {
final ConstPool cpool = this.c.getClassFile().getConstPool();
final AnnotationsAttribute attr = new AnnotationsAttribute(cpool, AnnotationsAttribute.visibleTag);
for(String anno: _annotations) {
final Annotation annot = new Annotation(anno, cpool);
attr.addAnnotation(annot);
}
_fld.getFieldInfo().addAttribute(attr);
}
}
/**
* addIntMember.
*
* @param _field_name a {@link java.lang.String} object.
* @param _final a boolean.
* @throws javassist.CannotCompileException if any.
*/
public synchronized void addIntMember(String _field_name, boolean _final) throws CannotCompileException {
final CtField f = new CtField(CtClass.intType, _field_name, this.c);
if(!_final)
f.setModifiers(Modifier.PUBLIC | Modifier.STATIC | Modifier.TRANSIENT);
else
f.setModifiers(Modifier.PUBLIC | Modifier.STATIC | Modifier.TRANSIENT | Modifier.FINAL);
// Avoid problems with JDO/JPA
this.addFieldAnnotations(f, this.fieldAnnotations);
this.c.addField(f, "0");
}
/**
* Generates a name for a class member.
*
* @param _prefix a {@link java.lang.String} object.
* @param _construct_name a {@link java.lang.String} object.
* @param _random_part a boolean.
* @return a {@link java.lang.String} object.
*/
public String getUniqueMemberName(String _prefix, String _construct_name, boolean _random_part) {
final StringBuffer b = new StringBuffer();
if(_prefix!=null) b.append(_prefix);
if(_construct_name!=null) {
if(_prefix!=null) b.append("_");
// check if is a clinit removing <>
_construct_name = _construct_name.replace("<", "").replace(">", "");
b.append(_construct_name.toUpperCase());
}
if(_random_part) {
if(_prefix!=null || _construct_name!=null) b.append("_");
final String rnd = String.valueOf(Math.random()*10000000);
b.append(rnd.substring(0, rnd.indexOf('.')));
}
return b.toString();
}
/**
* From http://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.8:
*
* The "Java letters" include uppercase and lowercase ASCII Latin letters A-Z (\u0041-\u005a),
* and a-z (\u0061-\u007a), and, for historical reasons, the ASCII underscore (_, or \u005f) and dollar sign ($, or \u0024).
* The $ character should be used only in mechanically generated source code or, rarely, to access pre-existing names on legacy systems.
* The "Java digits" include the ASCII digits 0-9 (\u0030-\u0039).
*/
private static Pattern QUALIFIED_TYPE_PATTERN = null;
private static Pattern getClassPattern() {
if(QUALIFIED_TYPE_PATTERN==null)
QUALIFIED_TYPE_PATTERN = Pattern.compile("([0-9a-zA-Z_\\.\\$]*\\.)([a-zA-Z0-9_\\$]*)");
return QUALIFIED_TYPE_PATTERN;
}
private static Pattern NESTED_CLASS_PATTERN = null;
private static Pattern getNestedClassPattern() {
if(NESTED_CLASS_PATTERN==null)
NESTED_CLASS_PATTERN = Pattern.compile("([0-9a-zA-Z_\\$]*\\$)([a-zA-Z0-9_]*)");
return NESTED_CLASS_PATTERN;
}
/**
* Removes package information (if any) from method and constructor parameters.
* Previously, a string tokenizer split the given {@link String} at every comma,
* which led to problems in case of Map parameters (e.g., Map<String,Object>).
* The current implementation uses the regular expression {@link ClassVisitor#QUALIFIED_TYPE_PATTERN}.
*
* @param _string a String with qualified parameter types
* @return a a String where the package info of qualified parameter types has been removed
*/
public static String removeParameterQualification(String _string) {
final StringBuilder b = new StringBuilder();
// Get everything between the brackets
final int i = _string.indexOf("(");
final int j = _string.lastIndexOf(")");
if(i==-1 || j==-1)
throw new IllegalArgumentException("Method has no round brackets: [" + _string + "]");
b.append(_string.substring(0, i+1));
b.append(ClassVisitor.removePackageContext(_string.substring(i+1, j)));
b.append(_string.substring(j));
return b.toString();
}
/**
* removePackageContext.
*
* @param _string a {@link java.lang.String} object.
* @return a {@link java.lang.String} object.
*/
public static String removePackageContext(String _string) {
final StringBuilder b = new StringBuilder();
// Find and replace pattern
final Matcher m = ClassVisitor.getClassPattern().matcher(_string);
Matcher nested_class_matcher = null;
int idx = 0;
String class_name = null;
while(m.find()) {
b.append(_string.substring(idx, m.start()));
// Found class name, now check if nested
class_name = m.group(2);
nested_class_matcher = ClassVisitor.getNestedClassPattern().matcher(class_name);
if(nested_class_matcher.matches()) {
b.append(nested_class_matcher.group(2));
}
else {
b.append(class_name);
}
idx = m.end();
}
// Append the remainders
b.append(_string.substring(idx));
return b.toString();
}
/**
* Getter for the field javaId
.
*
* @return a {@link com.sap.psr.vulas.java.JavaId} object.
*/
public JavaId getJavaId() { return this.javaId; }
/**
* getCtClass.
*
* @return a {@link javassist.CtClass} object.
*/
public CtClass getCtClass() { return this.c; }
/**
* getArchiveDigest.
*
* @return a {@link java.lang.String} object.
*/
public String getArchiveDigest() { return this.originalArchiveDigest; }
/**
* Getter for the field appContext
.
*
* @return a {@link com.sap.psr.vulas.shared.json.model.Application} object.
*/
public Application getAppContext() { return this.appContext; }
/**
* Getter for the field qname
.
*
* @return a {@link java.lang.String} object.
*/
public String getQname() { return this.qname; }
/**
* Getter for the field originalArchiveDigest
.
*
* @return a {@link java.lang.String} object.
*/
public String getOriginalArchiveDigest() { return this.originalArchiveDigest; }
/**
* prettyPrint.
*
* @param _src a {@link java.lang.String} object.
* @return a {@link java.lang.String} object.
*/
public final static String prettyPrint(String _src) {
final String n = System.getProperty("line.separator");
final String indent = " ";
final StringBuffer b = new StringBuffer();
int lvl = 0;
for(int i=0; i<_src.length(); i++) {
char c = _src.charAt(i);
switch(c) {
case ';':
b.append(c).append(n).append(getIndent(indent, lvl)); break;
case '{':
b.append(c).append(n).append(getIndent(indent, ++lvl)); break;
case '}':
b.append(c).append(n).append(getIndent(indent, --lvl)); break;
default:
b.append(c);
}
}
return b.toString();
}
private static String getIndent(String _c, int _i) {
final StringBuffer b = new StringBuffer();
for(int i=0; i<_i; i++)
b.append(_c);
return b.toString();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy