/*
* Copyright 2016, Stuart Douglas, and individual contributors as indicated
* by the @authors tag.
*
* 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.
*/
package org.fakereplace.manip;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.fakereplace.api.environment.CurrentEnvironment;
import org.fakereplace.core.Constants;
import org.fakereplace.data.BaseClassData;
import org.fakereplace.data.ClassDataStore;
import org.fakereplace.data.MethodData;
import org.fakereplace.logging.Logger;
import org.fakereplace.manip.data.ConstructorRewriteData;
import org.fakereplace.manip.util.ManipulationDataStore;
import org.fakereplace.manip.util.ManipulationUtils;
import org.fakereplace.runtime.MethodIdentifierStore;
import javassist.bytecode.Bytecode;
import javassist.bytecode.ClassFile;
import javassist.bytecode.CodeIterator;
import javassist.bytecode.ConstPool;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.Opcode;
public class ConstructorInvocationManipulator implements ClassManipulator {
private static final Logger log = Logger.getLogger(ConstructorInvocationManipulator.class);
private final ManipulationDataStore<ConstructorRewriteData> data = new ManipulationDataStore<ConstructorRewriteData>();
public synchronized void clearRewrites(String className, ClassLoader loader) {
data.remove(className, loader);
}
/**
* This class re-writes constructor access. It is more complex than other
* manipulators as the work can't be hidden away in a temporary class
*/
public void rewriteConstructorCalls(String clazz, String descriptor, int methodNo, ClassLoader classLoader) {
ConstructorRewriteData d = new ConstructorRewriteData(clazz, descriptor, methodNo, classLoader);
data.add(clazz, d);
}
public boolean transformClass(ClassFile file, ClassLoader loader, boolean modifiableClass, final Set<MethodInfo> modifiedMethods) {
Map<String, Set<ConstructorRewriteData>> constructorRewrites = new HashMap<>(data.getManipulationData(loader));
Map<Integer, ConstructorRewriteData> methodCallLocations = new HashMap<Integer, ConstructorRewriteData>();
// first we need to scan the constant pool looking for
// CONSTANT_method_info_ref structures
ConstPool pool = file.getConstPool();
for (int i = 1; i < pool.getSize(); ++i) {
// we have a method call
if (pool.getTag(i) == ConstPool.CONST_Methodref) {
boolean handled = false;
String className = pool.getMethodrefClassName(i);
String methodDesc = pool.getMethodrefType(i);
String methodName = pool.getMethodrefName(i);
if(methodName.equals("<init>")) {
if (constructorRewrites.containsKey(className)) {
for (ConstructorRewriteData data : constructorRewrites.get(className)) {
if (methodDesc.equals(data.getMethodDesc())) {
// store the location in the const pool of the method ref
methodCallLocations.put(i, data);
// we have found a method call
// now lets replace it
handled = true;
break;
}
}
}
if (!handled && CurrentEnvironment.getEnvironment().isClassReplaceable(className, loader)) {
//may be an added field
//if the field does not actually exist yet we just assume it is about to come into existence
//and rewrite it anyway
BaseClassData data = ClassDataStore.instance().getBaseClassData(loader, className);
if (data != null) {
MethodData method = data.getMethodOrConstructor("<init>", methodDesc);
if (method == null) {
//this is a new method
//lets deal with it
int methodNo = MethodIdentifierStore.instance().getMethodNumber("<init>", methodDesc);
methodCallLocations.put(i, new ConstructorRewriteData(className, methodDesc, methodNo, loader));
}
}
}
}
}
}
// this means we found an instance of the call, now we have to iterate
// through the methods and replace instances of the call
if (!methodCallLocations.isEmpty()) {
List<MethodInfo> methods = file.getMethods();
for (MethodInfo m : methods) {
try {
// ignore abstract methods
if (m.getCodeAttribute() == null) {
continue;
}
CodeIterator it = m.getCodeAttribute().iterator();
while (it.hasNext()) {
// loop through the bytecode
int index = it.next();
int op = it.byteAt(index);
// if the bytecode is a method invocation
if (op == CodeIterator.INVOKESPECIAL) {
int val = it.s16bitAt(index + 1);
// if the method call is one of the methods we are
// replacing
if (methodCallLocations.containsKey(val)) {
ConstructorRewriteData data = methodCallLocations.get(val);
// so we currently have all the arguments sitting on the
// stack, and we need to jigger them into
// an array and then call our method. First thing to do
// is scribble over the existing
// instructions:
it.writeByte(CodeIterator.NOP, index);
it.writeByte(CodeIterator.NOP, index + 1);
it.writeByte(CodeIterator.NOP, index + 2);
Bytecode bc = new Bytecode(file.getConstPool());
ManipulationUtils.pushParametersIntoArray(bc, data.getMethodDesc());
// so now our stack looks like unconstructed instance : array
// we need unconstructed instance : int : array : null
bc.addIconst(data.getMethodNo());
bc.add(Opcode.SWAP);
bc.add(Opcode.ACONST_NULL);
bc.addInvokespecial(data.getClazz(), "<init>", Constants.ADDED_CONSTRUCTOR_DESCRIPTOR);
// and we have our bytecode
it.insert(bc.get());
modifiedMethods.add(m);
}
}
}
} catch (Exception e) {
log.error("Bad byte code transforming " + file.getName(), e);
}
}
return true;
} else {
return false;
}
}
}