package net.sourceforge.retroweaver;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import net.sourceforge.retroweaver.event.WeaveListener;
import net.sourceforge.retroweaver.optimizer.ClassConstantsCollector;
import net.sourceforge.retroweaver.optimizer.Constant;
import net.sourceforge.retroweaver.optimizer.ConstantComparator;
import net.sourceforge.retroweaver.optimizer.ConstantPool;
import net.sourceforge.retroweaver.translator.NameSpace;
import net.sourceforge.retroweaver.translator.NameTranslator;
import net.sourceforge.retroweaver.translator.NameTranslatorClassVisitor;
import net.sourceforge.retroweaver.translator.TranslatorException;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
/**
* A bytecode enhancer that translates Java 1.5 class files into Java 1.4 class
* files. The enhancer performs primarily two tasks: 1) Reverses changes made to
* the class file format in 1.5 to the former 1.4 format. 2) Replaces compiler
* generated calls into the new 1.5 runtime with calls into RetroWeaver's
* replacement runtime.
*/
public class RetroWeaver {
private final int target;
private boolean lazy;
/**
* Indicates whether the generic signatures should be stripped. Default to <code>false</code>.
*/
private boolean stripSignatures;
/**
* Indicates whether the custom retroweaver attributes should be stripped. Default to <code>false</code>.
*/
private boolean stripAttributes;
private int weavedClassCount;
private WeaveListener listener;
private RefVerifier verifier;
private static final String newLine = System.getProperty("line.separator");
public RetroWeaver(int target) {
this.target = target;
}
protected static final FileFilter classFilter = new FileFilter() {
public boolean accept(File f) {
return f.getName().endsWith(".class");
}
};
protected static final FileFilter subdirFilter = new FileFilter() {
public boolean accept(File f) {
return f.isDirectory();
}
};
protected static void buildFileSets(ArrayList<File[]> fileSets, File path) {
File[] files = path.listFiles(classFilter);
if (files != null) {
fileSets.add(files);
}
File[] subdirs = path.listFiles(subdirFilter);
if (subdirs != null) {
for (File subdir : subdirs) {
buildFileSets(fileSets, subdir);
}
}
}
private void displayStartMessage(int n) {
if (n > 0) {
listener.weavingStarted("Processing " + n + (n == 1?" class":" classes"));
}
}
private void displayEndMessage() {
if (weavedClassCount > 0) {
listener.weavingCompleted(Integer.toString(weavedClassCount) + (weavedClassCount == 1?" class":" classes") + " weaved.");
}
}
public void weave(File path) throws IOException {
ArrayList<File[]> fileSets = new ArrayList<File[]>();
buildFileSets(fileSets, path);
int n = 0;
for (File[] set : fileSets) {
n += set.length;
}
displayStartMessage(n);
for (int i = 0; i < fileSets.size(); i++) {
for (File file : fileSets.get(i)) {
String sourcePath = file.getCanonicalPath();
weave(sourcePath, null);
}
}
displayEndMessage();
if (verifier != null) {
verifier.verifyFiles();
verifier.displaySummary();
}
}
public void weave(File[] baseDirs, String[][] fileSets, File outputDir)
throws IOException {
int n = 0;
for (String[] set : fileSets) {
n += set.length;
}
displayStartMessage(n);
Set<String> weaved = new HashSet<String>();
for (int i = 0; i < fileSets.length; i++) {
for (String fileName : fileSets[i]) {
File file = new File(baseDirs[i], fileName);
String sourcePath = file.getCanonicalPath();
String outputPath = null;
if (outputDir != null) {
outputPath = new File(outputDir, fileName)
.getCanonicalPath();
}
// Weave it unless already weaved.
if (!weaved.contains(sourcePath)) {
weave(sourcePath, outputPath);
weaved.add(sourcePath);
}
}
}
displayEndMessage();
if (verifier != null) {
verifier.verifyFiles();
verifier.displaySummary();
}
}
public void weaveJarFile(String sourceJarFileName, String destJarFileName)
throws IOException {
JarFile jarFile = new JarFile(sourceJarFileName);
ArrayList<JarEntry> entries = Collections.list(jarFile.entries());
OutputStream os = new FileOutputStream(destJarFileName);
JarOutputStream out = new JarOutputStream(os);
int n = 0;
for (JarEntry entry : entries) {
if (entry.getName().endsWith(".class")) {
n++;
}
}
displayStartMessage(n);
for (JarEntry entry : entries) {
String name = entry.getName();
InputStream dataStream = null;
if (name.endsWith(".class")) {
// weave class
InputStream is = jarFile.getInputStream(entry);
ByteArrayOutputStream classStream = new ByteArrayOutputStream();
if (weave(is, name, classStream)) {
// class file was modified
weavedClassCount++;
dataStream = new ByteArrayInputStream(classStream
.toByteArray());
// create new entry
entry = new JarEntry(name);
recordFileForVerifier(name);
}
}
if (dataStream == null) {
// not a class file or class wasn't no
dataStream = jarFile.getInputStream(entry);
}
// writing entry
out.putNextEntry(new JarEntry(name));
// writing data
int len;
final byte[] buf = new byte[1024];
while ((len = dataStream.read(buf)) >= 0) {
out.write(buf, 0, len);
}
}
out.close();
jarFile.close();
displayEndMessage();
if (verifier != null) {
verifier.verifyJarFile(destJarFileName);
verifier.displaySummary();
}
}
public void weave(String sourcePath, String outputPath) throws IOException {
InputStream is = new FileInputStream(sourcePath);
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
if (weave(is, sourcePath, bos)) {
// new class was generated
weavedClassCount++;
String path;
if (outputPath == null) {
path = sourcePath;
} else {
path = outputPath;
// create parent dir if necessary
File parentDir = new File(path).getParentFile();
if (parentDir != null) {
parentDir.mkdirs();
}
}
FileOutputStream fos = new FileOutputStream(path);
fos.write(bos.toByteArray());
fos.close();
recordFileForVerifier(path);
} else {
// We're lazy and the class already has the target version.
if (outputPath == null) {
// weaving in place
return;
}
File dir = new File(outputPath).getParentFile();
if (dir != null) {
dir.mkdirs();
}
File sf = new File(sourcePath);
File of = new File(outputPath);
if (!of.isFile()
|| !of.getCanonicalPath().equals(sf.getCanonicalPath())) {
// Target doesn't exist or is different from source so copy
// the file and transfer utime.
FileInputStream fis = new FileInputStream(sf);
byte[] bytes = new byte[(int) sf.length()];
fis.read(bytes);
fis.close();
FileOutputStream fos = new FileOutputStream(of);
fos.write(bytes);
fos.close();
of.setLastModified(sf.lastModified());
}
}
} finally {
try {
is.close();
} catch (IOException e) { // NOPMD by xlv
}
}
}
private void recordFileForVerifier(String fileName) {
if (verifier != null) {
verifier.addClass(fileName);
}
}
private static final boolean COMPACT_CONSTANTS = true;
protected static final Attribute[] CUSTOM_ATTRIBUTES = {
new RetroWeaverAttribute(Weaver.getBuildNumber(), Weaver.VERSION_1_5)
};
private boolean classpathChecked;
private boolean isRuntimeInClassPath() {
if (!classpathChecked) {
try {
Class.forName("net.sourceforge.retroweaver.runtime.java.lang.annotation.AIB");
classpathChecked = true;
} catch (ClassNotFoundException e) {
listener.weavingError("Error: the retroweaver runtime must be in the classpath");
return false;
}
}
return true;
}
protected boolean weave(InputStream sourceStream, String fileName, ByteArrayOutputStream bos)
throws IOException {
if (!isRuntimeInClassPath()) {
return false;
}
ClassReader cr = new ClassReader(sourceStream);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
try {
// chain class visitors
ClassVisitor classVisitor = cw;
ConstantPool cp;
if (COMPACT_CONSTANTS) {
cp = new ConstantPool();
classVisitor = new ClassConstantsCollector(classVisitor, cp);
}
classVisitor = new NameTranslatorClassVisitor(classVisitor, NameTranslator.getGeneralTranslator());
classVisitor = new ClassWeaver(classVisitor,
lazy, stripAttributes, target, listener);
classVisitor = new NameTranslatorClassVisitor(classVisitor, NameTranslator.getHarmonyTranslator());
// StringBuilder translation will be done before general weaving and
// mirror translation: trimToSize() calls will be processed correctly
// and no need to do translations in general weaving process
classVisitor = new NameTranslatorClassVisitor(classVisitor, NameTranslator.getStringBuilderTranslator());
if (stripSignatures) {
classVisitor = new SignatureStripper(classVisitor);
}
cr.accept(classVisitor, CUSTOM_ATTRIBUTES, ClassReader.EXPAND_FRAMES);
if (COMPACT_CONSTANTS) {
Set<Constant> constants = new TreeSet<Constant>(new ConstantComparator());
constants.addAll(cp.values());
cr = new ClassReader(cw.toByteArray());
cw = new ClassWriter(0);
for(Constant c: constants) {
c.write(cw);
}
cr.accept(cw, 0);
}
bos.write(cw.toByteArray());
return true;
} catch (TranslatorException te) {
listener.weavingError(te.getMessage());
return false;
} catch (LazyException e) {
return false;
}
}
public void setListener(WeaveListener listener) {
this.listener = listener;
}
public void setLazy(boolean lazy) {
this.lazy = lazy;
}
public void setVerifier(RefVerifier verifier) {
this.verifier = verifier;
}
public static String getUsage() {
return "Usage: RetroWeaver " + newLine + " <source path>" + newLine
+ " [<output path>]";
}
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(getUsage()); // NOPMD by xlv
return;
}
String sourcePath = args[0];
String outputPath = null;
if (args.length > 1) {
outputPath = args[1];
}
try {
RetroWeaver weaver = new RetroWeaver(Weaver.VERSION_1_4);
weaver.setListener(new DefaultWeaveListener(false));
weaver.weave(sourcePath, outputPath);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @param stripSignatures The stripSignatures to set.
*/
public void setStripSignatures(boolean stripSignatures) {
this.stripSignatures = stripSignatures;
}
/**
* @param stripAttributes the stripAttributes to set
*/
public void setStripAttributes(boolean stripAttributes) {
this.stripAttributes = stripAttributes;
}
public void addNameSpaces(List<NameSpace> nameSpaces) {
NameTranslator translator = NameTranslator.getGeneralTranslator();
for(NameSpace n: nameSpaces) {
translator.addNameSpace(n);
}
}
}
class LazyException extends RuntimeException {
}
class ClassWeaver extends ClassAdapter implements Opcodes {
private final boolean lazy;
private final boolean stripAttributes;
private final int target;
private int originalClassVersion;
private final WeaveListener listener;
private String className;
private boolean isEnum;
private boolean isInterface;
private final Set<String> classLiteralCalls = new HashSet<String>();
public ClassWeaver(final ClassVisitor cv, boolean lazy, boolean stripAttributes, int target, WeaveListener listener) {
super(cv);
this.lazy = lazy;
this.stripAttributes = stripAttributes;
this.target = target;
this.listener = listener;
}
public void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces)
{
if (lazy && (version <= target)) {
// abort all visitors
throw new LazyException();
}
if (listener != null) {
listener.weavingPath(name);
}
className = name;
isEnum = superName != null && superName.equals("java/lang/Enum");
isInterface = (access & ACC_INTERFACE) == ACC_INTERFACE;
originalClassVersion = version;
cv.visit(target, // Changes the format of the class file from 1.5 to the target value.
access,
name,
signature,
superName,
interfaces);
}
public void visitInnerClass(
final String name,
final String outerName,
final String innerName,
final int access)
{
cv.visitInnerClass(name, outerName, innerName, access);
}
public FieldVisitor visitField(
final int access,
final String name,
final String desc,
final String signature,
final Object value)
{
return cv.visitField(access, name, desc, signature, value);
}
public MethodVisitor visitMethod(
final int access,
final String name,
final String desc,
final String signature,
final String[] exceptions)
{
int newAccess;
if ((access&(ACC_SYNTHETIC|ACC_BRIDGE)) == (ACC_SYNTHETIC|ACC_BRIDGE)) {
/*
bridge methods for generic create problems with RMIC code in 1.4.
It's a known bug with 1.4, see SUN's bug database at:
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4811083
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5035300
Problem found when implementing Comparable<E>, with bridge method
compareTo(Ljava/lang/Object;)I;
*/
// Workaround disabled so that isSynthethic() and isBridge() can be implemented
//newAccess = access & ~ACC_SYNTHETIC & ~ACC_BRIDGE;
newAccess = access;
if (name.equals(APPEND_METHOD) &&
(desc.equals(APPENDABLE_APPEND_SIGNATURE1) ||
desc.equals(APPENDABLE_APPEND_SIGNATURE2) ||
desc.equals(APPENDABLE_APPEND_SIGNATURE3))) {
/* remove bridge methods for Appendable, see Writer test case */
return null;
}
} else {
newAccess = access;
}
String[] newExceptions;
if (exceptions != null) {
newExceptions = new String[exceptions.length];
for (int i = 0; i < exceptions.length; i++) {
newExceptions[i] = NameTranslator.getGeneralTranslator().getClassMirrorTranslation(exceptions[i]);
}
} else {
newExceptions = exceptions;
}
MethodVisitor mv = new MethodWeaver(super.visitMethod(newAccess,
name,
desc,
signature,
newExceptions));
if (!isEnum || !"<clinit>".equals(name)) {
return mv;
}
return new EnumMethodWeaver(mv);
}
public void visitAttribute(final Attribute attr) {
if (attr instanceof RetroWeaverAttribute) {
// make sure the original version is kept if class file
// is weaved more than once
RetroWeaverAttribute ra = (RetroWeaverAttribute) attr;
originalClassVersion = ra.getOriginalClassVersion();
} else {
cv.visitAttribute(attr);
}
}
public void visitEnd() {
if (isEnum) {
cv.visitField(ACC_PRIVATE + ACC_STATIC + ACC_FINAL + ACC_SYNTHETIC,
SERIAL_ID_FIELD,
SERIAL_ID_SIGNATURE,
null, new Long(0L));
}
if (!classLiteralCalls.isEmpty()) {
// generate synthetic fields and class$ method
for(String fieldName: classLiteralCalls) {
FieldVisitor fv = visitField(ACC_STATIC + ACC_SYNTHETIC + ACC_FINAL
+ (isInterface?ACC_PUBLIC:ACC_PRIVATE),
fieldName,
CLASS_FIELD_DESC,
null, null);
fv.visitEnd();
}
}
if (!stripAttributes) {
RetroWeaverAttribute a = new RetroWeaverAttribute(Weaver.getBuildNumber(), originalClassVersion);
cv.visitAttribute(a);
}
cv.visitEnd();
}
/**
* Generate the byte code equivalent to ".class"
*
* @param mv method visitor to use
* @param cls name of class
*/
private void generateClassCall(MethodVisitor mv, String cls) {
/*
* generate the code equivalent to ".class"
*
new cls[0].getClass().getComponentType()
*/
mv.visitInsn (ICONST_0);
mv.visitTypeInsn (ANEWARRAY, cls);
mv.visitMethodInsn (INVOKEVIRTUAL, JAVA_LANG_OBJECT, GET_CLASS_METHOD, GET_CLASS_SIGNATURE);
mv.visitMethodInsn (INVOKEVIRTUAL, JAVA_LANG_CLASS, GET_COMPONENT_TYPE_METHOD, GET_COMPONENT_TYPE_SIGNATURE);
}
private class EnumMethodWeaver extends MethodAdapter implements Opcodes {
public EnumMethodWeaver(final MethodVisitor mv) {
super(mv);
}
public void visitInsn(final int opcode) {
if (opcode == RETURN) {
// add call to setEnumValues(Object[] values, Class c)
String owner = className.replace('.', '/');
String fullName = 'L' + owner + ';';
Type t = Type.getType(fullName);
mv.visitMethodInsn(INVOKESTATIC, owner, "values", "()[" + fullName);
mv.visitLdcInsn(t);
mv.visitMethodInsn( INVOKESTATIC, RETROWEAVER_ENUM,
"setEnumValues", "([Ljava/lang/Object;Ljava/lang/Class;)V" );
}
mv.visitInsn(opcode);
}
}
private static final String JAVA_LANG_CLASS = "java/lang/Class";
private static final String JAVA_LANG_OBJECT = "java/lang/Object";
private static final String GET_CLASS_METHOD = "getClass";
private static final String GET_CLASS_SIGNATURE = "()Ljava/lang/Class;";
private static final String GET_COMPONENT_TYPE_METHOD = "getComponentType";
private static final String GET_COMPONENT_TYPE_SIGNATURE = "()Ljava/lang/Class;";
private static final String SERIAL_ID_FIELD = "serialVersionUID";
private static final String SERIAL_ID_SIGNATURE = "J";
private static final String CLASS_FIELD_DESC = "Ljava/lang/Class;";
private static final String ITERABLE_CLASS = "java/lang/Iterable";
private static final String ITERATOR_METHOD = "iterator";
private static final String ITERATOR_SIGNATURE = "()Ljava/util/Iterator;";
private static final String ITERABLE_METHODS_CLASS = "net/sourceforge/retroweaver/runtime/java/lang/Iterable_";
private static final String ITERABLE_METHODS_ITERATOR_SIGNATURE = "(Ljava/lang/Object;)Ljava/util/Iterator;";
private static final String APPEND_METHOD = "append";
private static final String APPENDABLE_APPEND_SIGNATURE1 = "(C)Ljava/lang/Appendable;";
private static final String APPENDABLE_APPEND_SIGNATURE2 = "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;";
private static final String APPENDABLE_APPEND_SIGNATURE3 = "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;";
private static final String RETROWEAVER_ENUM = "net/sourceforge/retroweaver/runtime/java/lang/Enum";
private static final String REENTRANTREADWRITELOCK_CLASS = "java/util/concurrent/locks/ReentrantReadWriteLock";
private static final String REENTRANTREADWRITELOCK_READLOCK_CLASS = "java/util/concurrent/locks/ReentrantReadWriteLock$ReadLock";
private static final String REENTRANTREADWRITELOCK_WRITELOCK_CLASS = "java/util/concurrent/locks/ReentrantReadWriteLock$WriteLock";
private static final String READLOCK_METHOD = "readLock";
private static final String WRITELOCK_METHOD = "writeLock";
private static final String REENTRANTREADWRITELOCK_READLOCK_SIGNATURE = "()Ljava/util/concurrent/locks/ReentrantReadWriteLock$ReadLock;";
private static final String REENTRANTREADWRITELOCK_WRITELOCK_SIGNATURE = "()Ljava/util/concurrent/locks/ReentrantReadWriteLock$WriteLock;";
private static final String REENTRANTREADWRITELOCK_READLOCK_NEW_SIGNATURE = "()Ljava/util/concurrent/locks/Lock;";
private static final String REENTRANTREADWRITELOCK_WRITELOCK_NEW_SIGNATURE = "()Ljava/util/concurrent/locks/Lock;";
class MethodWeaver extends MethodAdapter implements Opcodes {
public MethodWeaver(final MethodVisitor mv) {
super(mv);
}
public void visitMethodInsn(
final int opcode,
final String owner,
final String name,
final String desc)
{
if (opcode == INVOKEINTERFACE &&
owner.equals(ITERABLE_CLASS) &&
name.equals(ITERATOR_METHOD) &&
desc.equals(ITERATOR_SIGNATURE)) {
super.visitMethodInsn(INVOKESTATIC,
ITERABLE_METHODS_CLASS,
ITERATOR_METHOD,
ITERABLE_METHODS_ITERATOR_SIGNATURE);
return;
} else if (opcode == INVOKEVIRTUAL &&
owner.equals(REENTRANTREADWRITELOCK_CLASS)) {
// workaround for ReentrantReadWriteLock readLock() and writeLock() incompatible return types
if (name.equals(READLOCK_METHOD) && desc.equals(REENTRANTREADWRITELOCK_READLOCK_SIGNATURE)) {
super.visitMethodInsn(opcode, owner, name, REENTRANTREADWRITELOCK_READLOCK_NEW_SIGNATURE);
super.visitTypeInsn(CHECKCAST, REENTRANTREADWRITELOCK_READLOCK_CLASS);
return;
} else if (name.equals(WRITELOCK_METHOD) && desc.equals(REENTRANTREADWRITELOCK_WRITELOCK_SIGNATURE)) {
super.visitMethodInsn(opcode, owner, name, REENTRANTREADWRITELOCK_WRITELOCK_NEW_SIGNATURE);
super.visitTypeInsn(CHECKCAST, REENTRANTREADWRITELOCK_WRITELOCK_CLASS);
return;
}
}
// not a special case, use default implementation
super.visitMethodInsn(opcode, owner, name, desc);
}
public void visitLdcInsn(final Object cst) {
if (cst instanceof Type) {
/**
* Fix class literals. The 1.5 VM has had its ldc* instructions updated so
* that it knows how to deal with CONSTANT_Class in addition to the other
* types. So, we have to search for uses of ldc* that point to a
* CONSTANT_Class and replace them with synthetic field access the way
* it was generated in 1.4.
*/
// LDC or LDC_W with a class as argument
Type t = (Type) cst;
String fieldName = getClassLiteralFieldName(t);
classLiteralCalls.add(fieldName);
mv.visitFieldInsn(GETSTATIC, className, fieldName, CLASS_FIELD_DESC);
mv.visitInsn(DUP);
Label nonNullLabel = new Label();
mv.visitJumpInsn(IFNONNULL, nonNullLabel);
mv.visitInsn(POP);
String s;
if (t.getSort() == Type.OBJECT) {
s = t.getInternalName();
} else {
s = t.getDescriptor();
}
/* convert retroweaver runtime classes:
* Enum into net.sourceforge.retroweaver.runtime.Enum_
* concurrent classes into their backport equivalent
* ...
*/
s = NameTranslator.getGeneralTranslator().getClassMirrorTranslationDescriptor(s);
s = NameTranslator.getStringBuilderTranslator().getClassMirrorTranslationDescriptor(s);
s = NameTranslator.getHarmonyTranslator().getClassMirrorTranslationDescriptor(s);
generateClassCall(mv, s);
mv.visitInsn(DUP);
mv.visitFieldInsn(PUTSTATIC, className, fieldName, CLASS_FIELD_DESC);
mv.visitLabel(nonNullLabel);
} else {
super.visitLdcInsn(cst);
}
}
private String getClassLiteralFieldName(Type type) {
String fieldName;
if (type.getSort() == Type.ARRAY) {
fieldName = "array" + type.getDescriptor().replace('[', '$');
if (fieldName.charAt(fieldName.length()-1) == ';') {
fieldName = fieldName.substring(0, fieldName.length()-1);
}
} else {
fieldName = "class$" + type.getInternalName();
}
fieldName = fieldName.replace('/', '$');
return fieldName;
}
}
}
class DefaultWeaveListener implements WeaveListener {
private final boolean verbose;
DefaultWeaveListener(boolean verbose) {
this.verbose = verbose;
}
public void weavingStarted(String msg) {
System.out.println("[RetroWeaver] " + msg); // NOPMD by xlv
}
public void weavingCompleted(String msg) {
System.out.println("[RetroWeaver] " + msg); // NOPMD by xlv
}
public void weavingError(String msg) {
System.out.println("[RetroWeaver] " + msg); // NOPMD by xlv
}
public void weavingPath(String sourcePath) {
if (verbose) {
System.out.println("[RetroWeaver] Weaving " + sourcePath); // NOPMD by xlv
}
}
}