/*
* Copyright 2011 the original author or authors.
*
* 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.powermock.core;
import javassist.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This class takes care of creating a replica of a class. The class structure
* is copied to the new class. This is useful in situations where you want to
* create a mock for a class but it's not possible because of some restrictions
* (such as the class being loaded by the bootstrap class-loader).
*/
public class ClassReplicaCreator {
private static final String POWERMOCK_INSTANCE_DELEGATOR_FIELD_NAME = "powerMockInstanceDelegatorField";
// Used to make each new replica class of a specific type unique.
private static AtomicInteger counter = new AtomicInteger(0);
public Class<?> createClassReplica(Class<?> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("clazz cannot be null");
}
ClassPool classpool = ClassPool.getDefault();
final String originalClassName = clazz.getName();
CtClass originalClassAsCtClass;
final CtClass newClass = classpool.makeClass(generateReplicaClassName(clazz));
try {
originalClassAsCtClass = classpool.get(originalClassName);
CtMethod[] declaredMethods = originalClassAsCtClass.getDeclaredMethods();
for (CtMethod ctMethod : declaredMethods) {
final String code = getReplicaMethodDelegationCode(clazz, ctMethod, null);
CtNewMethod.make(ctMethod.getReturnType(), ctMethod.getName(), ctMethod.getParameterTypes(), ctMethod.getExceptionTypes(),
code, newClass);
}
return newClass.toClass(this.getClass().getClassLoader(), this.getClass().getProtectionDomain());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Create a class that is a replica of type {@code T}. To allow for
* partial mocking all calls to non-mocked methods will be delegated to the
* {@code delegator}.
*
* @param <T> The type of the replica class to be created.
* @param delegator The delegator object that will be invoked to allow for partial
* mocking.
* @return A replica class that can be used to duck-type an instance.
*/
@SuppressWarnings("unchecked")
public <T> Class<T> createInstanceReplica(T delegator) {
if (delegator == null) {
throw new IllegalArgumentException("delegator cannot be null");
}
final Class<T> clazz = (Class<T>) delegator.getClass();
ClassPool classpool = ClassPool.getDefault();
final String originalClassName = clazz.getName();
CtClass originalClassAsCtClass;
final CtClass newClass = classpool.makeClass(generateReplicaClassName(clazz));
try {
originalClassAsCtClass = classpool.get(originalClassName);
copyFields(originalClassAsCtClass, newClass);
addDelegatorField(delegator, newClass);
CtMethod[] declaredMethods = originalClassAsCtClass.getDeclaredMethods();
for (CtMethod ctMethod : declaredMethods) {
@SuppressWarnings("unused")
final String code = getReplicaMethodDelegationCode(delegator.getClass(), ctMethod, POWERMOCK_INSTANCE_DELEGATOR_FIELD_NAME);
CtMethod make2 = CtNewMethod.copy(ctMethod, newClass, null);
newClass.addMethod(make2);
}
CtConstructor[] declaredConstructors = originalClassAsCtClass.getDeclaredConstructors();
for (CtConstructor ctConstructor : declaredConstructors) {
CtConstructor copy = CtNewConstructor.copy(ctConstructor, newClass, null);
newClass.addConstructor(copy);
}
return newClass.toClass(this.getClass().getClassLoader(), this.getClass().getProtectionDomain());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Add a field to the replica class that holds the instance delegator. I.e.
* if we're creating a instance replica of {@code java.lang.Long} this
* methods adds a new field of type {@code delegator.getClass()} to the
* replica class.
*/
private <T> void addDelegatorField(T delegator, final CtClass replicaClass) throws CannotCompileException {
CtField f = CtField.make(String.format("private %s %s = null;", delegator.getClass().getName(),
POWERMOCK_INSTANCE_DELEGATOR_FIELD_NAME), replicaClass);
replicaClass.addField(f);
}
private <T> String generateReplicaClassName(final Class<T> clazz) {
return "replica." + clazz.getName() + "$$PowerMock" + counter.getAndIncrement();
}
private void copyFields(CtClass originalClassAsCtClass, final CtClass newClass) throws CannotCompileException, NotFoundException {
CtField[] declaredFields = originalClassAsCtClass.getDeclaredFields();
CtField[] undeclaredFields = originalClassAsCtClass.getFields();
Set<CtField> allFields = new HashSet<CtField>();
Collections.addAll(allFields, declaredFields);
Collections.addAll(allFields, undeclaredFields);
for (CtField ctField : allFields) {
CtField f = new CtField(ctField.getType(), ctField.getName(), newClass);
newClass.addField(f);
}
}
/*
* Invokes a instance method of the original instance. This enables partial
* mocking of system classes.
*/
private String getReplicaMethodDelegationCode(Class<?> clazz, CtMethod ctMethod, String classOrInstanceToDelegateTo)
throws NotFoundException {
StringBuilder builder = new StringBuilder();
builder.append("{java.lang.reflect.Method originalMethod = ");
builder.append(clazz.getName());
builder.append(".class.getDeclaredMethod(\"");
builder.append(ctMethod.getName());
builder.append("\", ");
final String parametersAsString = getParametersAsString(getParameterTypes(ctMethod));
if ("".equals(parametersAsString)) {
builder.append("null");
} else {
builder.append(parametersAsString);
}
builder.append(");\n");
builder.append("originalMethod.setAccessible(true);\n");
final CtClass returnType = ctMethod.getReturnType();
final boolean isVoid = returnType.equals(CtClass.voidType);
if (!isVoid) {
builder.append("return (");
builder.append(returnType.getName());
builder.append(") ");
}
builder.append("originalMethod.invoke(");
if (Modifier.isStatic(ctMethod.getModifiers()) || classOrInstanceToDelegateTo == null) {
builder.append(clazz.getName());
builder.append(".class");
} else {
builder.append(classOrInstanceToDelegateTo);
}
builder.append(", $args);}");
return builder.toString();
}
private String[] getParameterTypes(CtMethod ctMethod) throws NotFoundException {
final CtClass[] parameterTypesAsCtClass = ctMethod.getParameterTypes();
final String[] parameterTypes = new String[parameterTypesAsCtClass.length];
for (int i = 0; i < parameterTypes.length; i++) {
parameterTypes[i] = parameterTypesAsCtClass[i].getName() + ".class";
}
return parameterTypes;
}
private static String getParametersAsString(String[] types) {
StringBuilder parametersAsString = new StringBuilder();
if (types != null && types.length == 0) {
parametersAsString.append("new Class[0]");
} else {
parametersAsString.append("new Class[] {");
if (types != null) {
for (int i = 0; i < types.length; i++) {
parametersAsString.append(types[i]);
if (i != types.length - 1) {
parametersAsString.append(", ");
}
}
}
parametersAsString.append("}");
}
return parametersAsString.toString();
}
}