blob: 8ee547f23854c641739018e1a869fd6442df5efb [file] [log] [blame]
// Copyright 2011-2013 The Apache Software Foundation
//
// 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.apache.tapestry5.internal.plastic;
import org.apache.tapestry5.internal.plastic.asm.ClassReader;
import org.apache.tapestry5.internal.plastic.asm.ClassWriter;
import org.apache.tapestry5.internal.plastic.asm.Opcodes;
import org.apache.tapestry5.internal.plastic.asm.tree.*;
import org.apache.tapestry5.plastic.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Responsible for managing a class loader that allows ASM {@link ClassNode}s
* to be instantiated as runtime classes.
*/
@SuppressWarnings("rawtypes")
public class PlasticClassPool implements ClassLoaderDelegate, Opcodes, PlasticClassListenerHub
{
private static final Logger LOGGER = LoggerFactory.getLogger(PlasticClassPool.class);
final PlasticClassLoader loader;
private final PlasticManagerDelegate delegate;
private final Set<String> controlledPackages;
private final Map<String, Boolean> checkedExceptionCache = new HashMap<String, Boolean>();
// Would use Deque, but that's added in 1.6 and we're still striving for 1.5 code compatibility.
private final Stack<String> activeInstrumentClassNames = new Stack<String>();
/**
* Maps class names to instantiators for that class name.
* Synchronized on the loader.
*/
private final Map<String, ClassInstantiator> instantiators = PlasticInternalUtils.newConcurrentMap();
private final InheritanceData emptyInheritanceData = new InheritanceData(null);
private final StaticContext emptyStaticContext = new StaticContext();
private final List<PlasticClassListener> listeners = new CopyOnWriteArrayList<PlasticClassListener>();
private final Cache<String, TypeCategory> typeName2Category = new Cache<String, TypeCategory>()
{
@Override
protected TypeCategory convert(String typeName)
{
ClassNode cn = constructClassNodeFromBytecode(typeName);
return Modifier.isInterface(cn.access) ? TypeCategory.INTERFACE : TypeCategory.CLASS;
}
};
static class BaseClassDef
{
final InheritanceData inheritanceData;
final StaticContext staticContext;
public BaseClassDef(InheritanceData inheritanceData, StaticContext staticContext)
{
this.inheritanceData = inheritanceData;
this.staticContext = staticContext;
}
}
/**
* Map from FQCN to BaseClassDef. Synchronized on the loader.
*/
private final Map<String, BaseClassDef> baseClassDefs = PlasticInternalUtils.newMap();
private final Map<String, FieldInstrumentations> instrumentations = PlasticInternalUtils.newMap();
private final Map<String, String> transformedClassNameToImplementationClassName = PlasticInternalUtils.newMap();
private final FieldInstrumentations placeholder = new FieldInstrumentations(null);
private final Set<TransformationOption> options;
/**
* Creates the pool with a set of controlled packages; all classes in the controlled packages are loaded by the
* pool's class loader, and all top-level classes in the controlled packages are transformed via the delegate.
*
* @param parentLoader
* typically, the Thread's context class loader
* @param delegate
* responsible for end stages of transforming top-level classes
* @param controlledPackages
* set of package names (note: retained, not copied)
* @param options
* used when transforming classes
*/
public PlasticClassPool(ClassLoader parentLoader, PlasticManagerDelegate delegate, Set<String> controlledPackages,
Set<TransformationOption> options)
{
loader = new PlasticClassLoader(parentLoader, this);
this.delegate = delegate;
this.controlledPackages = controlledPackages;
this.options = options;
}
public ClassLoader getClassLoader()
{
return loader;
}
public Class realizeTransformedClass(ClassNode classNode, InheritanceData inheritanceData,
StaticContext staticContext)
{
synchronized (loader)
{
Class result = realize(PlasticInternalUtils.toClassName(classNode.name), ClassType.PRIMARY, classNode);
baseClassDefs.put(result.getName(), new BaseClassDef(inheritanceData, staticContext));
return result;
}
}
public Class realize(String primaryClassName, ClassType classType, ClassNode classNode)
{
synchronized (loader)
{
if (!listeners.isEmpty())
{
fire(toEvent(primaryClassName, classType, classNode));
}
byte[] bytecode = toBytecode(classNode);
String className = PlasticInternalUtils.toClassName(classNode.name);
return loader.defineClassWithBytecode(className, bytecode);
}
}
private PlasticClassEvent toEvent(final String primaryClassName, final ClassType classType,
final ClassNode classNode)
{
return new PlasticClassEvent()
{
@Override
public ClassType getType()
{
return classType;
}
@Override
public String getPrimaryClassName()
{
return primaryClassName;
}
@Override
public String getDissasembledBytecode()
{
return PlasticInternalUtils.dissasembleBytecode(classNode);
}
@Override
public String getClassName()
{
return PlasticInternalUtils.toClassName(classNode.name);
}
};
}
private void fire(PlasticClassEvent event)
{
for (PlasticClassListener listener : listeners)
{
listener.classWillLoad(event);
}
}
private byte[] toBytecode(ClassNode classNode)
{
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
classNode.accept(writer);
return writer.toByteArray();
}
public AnnotationAccess createAnnotationAccess(String className)
{
try
{
final Class<?> searchClass = loader.loadClass(className);
return new AnnotationAccess()
{
@Override
public <T extends Annotation> boolean hasAnnotation(Class<T> annotationType)
{
return getAnnotation(annotationType) != null;
}
@Override
public <T extends Annotation> T getAnnotation(Class<T> annotationType)
{
return searchClass.getAnnotation(annotationType);
}
};
} catch (Exception ex)
{
throw new RuntimeException(ex);
}
}
public AnnotationAccess createAnnotationAccess(List<AnnotationNode> annotationNodes)
{
if (annotationNodes == null)
{
return EmptyAnnotationAccess.SINGLETON;
}
final Map<String, Object> cache = PlasticInternalUtils.newMap();
final Map<String, AnnotationNode> nameToNode = PlasticInternalUtils.newMap();
for (AnnotationNode node : annotationNodes)
{
nameToNode.put(PlasticInternalUtils.objectDescriptorToClassName(node.desc), node);
}
return new AnnotationAccess()
{
@Override
public <T extends Annotation> boolean hasAnnotation(Class<T> annotationType)
{
return nameToNode.containsKey(annotationType.getName());
}
@Override
public <T extends Annotation> T getAnnotation(Class<T> annotationType)
{
String className = annotationType.getName();
Object result = cache.get(className);
if (result == null)
{
result = buildAnnotation(className);
if (result != null)
cache.put(className, result);
}
return annotationType.cast(result);
}
private Object buildAnnotation(String className)
{
AnnotationNode node = nameToNode.get(className);
if (node == null)
return null;
return createAnnotation(className, node);
}
};
}
Class loadClass(String className)
{
try
{
return loader.loadClass(className);
} catch (Exception ex)
{
throw new RuntimeException(String.format("Unable to load class %s: %s", className,
PlasticInternalUtils.toMessage(ex)), ex);
}
}
protected Object createAnnotation(String className, AnnotationNode node)
{
AnnotationBuilder builder = new AnnotationBuilder(loadClass(className), this);
node.accept(builder);
return builder.createAnnotation();
}
@Override
public boolean shouldInterceptClassLoading(String className)
{
int searchFromIndex = className.length() - 1;
while (true)
{
int dotx = className.lastIndexOf('.', searchFromIndex);
if (dotx < 0)
break;
String packageName = className.substring(0, dotx);
if (controlledPackages.contains(packageName))
return true;
searchFromIndex = dotx - 1;
}
return false;
}
// Hopefully the synchronized will not cause a deadlock
@Override
public synchronized Class<?> loadAndTransformClass(String className) throws ClassNotFoundException
{
// Inner classes are not transformed, but they are loaded by the same class loader.
if (className.contains("$"))
{
return loadInnerClass(className);
}
// TODO: What about interfaces, enums, annotations, etc. ... they shouldn't be in the package, but
// we should generate a reasonable error message.
if (activeInstrumentClassNames.contains(className))
{
StringBuilder builder = new StringBuilder("");
String sep = "";
for (String name : activeInstrumentClassNames)
{
builder.append(sep);
builder.append(name);
sep = ", ";
}
throw new IllegalStateException(String.format("Unable to transform class %s as it is already in the process of being transformed; there is a cycle among the following classes: %s.",
className, builder));
}
activeInstrumentClassNames.push(className);
try
{
InternalPlasticClassTransformation transformation = getPlasticClassTransformation(className);
delegate.transform(transformation.getPlasticClass());
ClassInstantiator createInstantiator = transformation.createInstantiator();
ClassInstantiator configuredInstantiator = delegate.configureInstantiator(className, createInstantiator);
instantiators.put(className, configuredInstantiator);
return transformation.getTransformedClass();
} finally
{
activeInstrumentClassNames.pop();
}
}
private Class loadInnerClass(String className)
{
ClassNode classNode = constructClassNodeFromBytecode(className);
interceptFieldAccess(classNode);
return realize(className, ClassType.INNER, classNode);
}
private void interceptFieldAccess(ClassNode classNode)
{
for (MethodNode method : classNode.methods)
{
interceptFieldAccess(classNode.name, method);
}
}
private void interceptFieldAccess(String classInternalName, MethodNode method)
{
InsnList insns = method.instructions;
ListIterator it = insns.iterator();
while (it.hasNext())
{
AbstractInsnNode node = (AbstractInsnNode) it.next();
int opcode = node.getOpcode();
if (opcode != GETFIELD && opcode != PUTFIELD)
{
continue;
}
FieldInsnNode fnode = (FieldInsnNode) node;
String ownerInternalName = fnode.owner;
if (ownerInternalName.equals(classInternalName))
{
continue;
}
FieldInstrumentation instrumentation = getFieldInstrumentation(ownerInternalName, fnode.name, opcode == GETFIELD);
if (instrumentation == null)
{
continue;
}
// Replace the field access node with the appropriate method invocation.
insns.insertBefore(fnode, new MethodInsnNode(INVOKEVIRTUAL, ownerInternalName, instrumentation.methodName, instrumentation.methodDescription, false));
it.remove();
}
}
/**
* For a fully-qualified class name of an <em>existing</em> class, loads the bytes for the class
* and returns a PlasticClass instance.
*
* @throws ClassNotFoundException
*/
public InternalPlasticClassTransformation getPlasticClassTransformation(String className)
throws ClassNotFoundException
{
assert PlasticInternalUtils.isNonBlank(className);
ClassNode classNode = constructClassNodeFromBytecode(className);
String baseClassName = PlasticInternalUtils.toClassName(classNode.superName);
instrumentations.put(classNode.name, new FieldInstrumentations(classNode.superName));
// TODO: check whether second parameter should really be null
return createTransformation(baseClassName, classNode, null, false);
}
/**
* @param baseClassName
* class from which the transformed class extends
* @param classNode
* node for the class
* @param implementationClassNode
* node for the implementation class. May be null.
* @param proxy
* if true, the class is a new empty class; if false an existing class that's being transformed
* @throws ClassNotFoundException
*/
private InternalPlasticClassTransformation createTransformation(String baseClassName, ClassNode classNode, ClassNode implementationClassNode, boolean proxy)
throws ClassNotFoundException
{
if (shouldInterceptClassLoading(baseClassName))
{
loader.loadClass(baseClassName);
BaseClassDef def = baseClassDefs.get(baseClassName);
assert def != null;
return new PlasticClassImpl(classNode, implementationClassNode, this, def.inheritanceData, def.staticContext, proxy);
}
// When the base class is Object, or otherwise not in a transformed package,
// then start with the empty
return new PlasticClassImpl(classNode, implementationClassNode, this, emptyInheritanceData, emptyStaticContext, proxy);
}
/**
* Constructs a class node by reading the raw bytecode for a class and instantiating a ClassNode
* (via {@link ClassReader#accept(org.apache.tapestry5.internal.plastic.asm.ClassVisitor, int)}).
*
* @param className
* fully qualified class name
* @return corresponding ClassNode
*/
public ClassNode constructClassNodeFromBytecode(String className)
{
byte[] bytecode = readBytecode(className);
if (bytecode == null)
return null;
return PlasticInternalUtils.convertBytecodeToClassNode(bytecode);
}
private byte[] readBytecode(String className)
{
ClassLoader parentClassLoader = loader.getParent();
return PlasticInternalUtils.readBytecodeForClass(parentClassLoader, className, true);
}
public PlasticClassTransformation createTransformation(String baseClassName, String newClassName)
{
return createTransformation(baseClassName, newClassName, null);
}
public PlasticClassTransformation createTransformation(String baseClassName, String newClassName, String implementationClassName)
{
try
{
ClassNode newClassNode = new ClassNode();
final String internalNewClassNameinternalName = PlasticInternalUtils.toInternalName(newClassName);
final String internalBaseClassName = PlasticInternalUtils.toInternalName(baseClassName);
newClassNode.visit(PlasticConstants.DEFAULT_VERSION_OPCODE, ACC_PUBLIC, internalNewClassNameinternalName, null, internalBaseClassName, null);
ClassNode implementationClassNode = null;
if (implementationClassName != null)
{
// When decorating or advising a service, implementationClassName is the name
// of a proxy class already, such as "$ServiceName_[random string]",
// which doesn't exist as a file in the classpath, just in memory.
// So we need to keep what's the original implementation class name
// for each proxy, even a proxy around a proxy.
if (transformedClassNameToImplementationClassName.containsKey(implementationClassName))
{
implementationClassName =
transformedClassNameToImplementationClassName.get(implementationClassName);
}
if (!implementationClassName.startsWith("com.sun.proxy"))
{
try
{
implementationClassNode = readClassNode(implementationClassName);
} catch (IOException e)
{
LOGGER.warn(String.format("Unable to load class %s as the implementation of service %s",
implementationClassName, baseClassName));
// Go on. Probably a proxy class
}
}
transformedClassNameToImplementationClassName.put(newClassName, implementationClassName);
}
return createTransformation(baseClassName, newClassNode, implementationClassNode, true);
} catch (ClassNotFoundException ex)
{
throw new RuntimeException(String.format("Unable to create class %s as sub-class of %s: %s", newClassName,
baseClassName, PlasticInternalUtils.toMessage(ex)), ex);
}
}
private ClassNode readClassNode(String className) throws IOException
{
return readClassNode(className, getClassLoader());
}
static ClassNode readClassNode(String className, ClassLoader classLoader) throws IOException
{
ClassNode classNode = new ClassNode();
final String location = PlasticInternalUtils.toInternalName(className) + ".class";
InputStream inputStream = classLoader.getResourceAsStream(location);
BufferedInputStream bis = new BufferedInputStream(inputStream);
ClassReader classReader = new ClassReader(inputStream);
inputStream.close();
bis.close();
classReader.accept(classNode, 0);
return classNode;
}
public ClassInstantiator getClassInstantiator(String className)
{
ClassInstantiator result = instantiators.get(className);
if (result == null)
{
try
{
loader.loadClass(className);
result = instantiators.get(className);
} catch (ClassNotFoundException ex)
{
throw new RuntimeException(ex);
}
}
if (result != null)
{
return result;
}
// TODO: Verify that the problem is incorrect package, and not any other failure.
StringBuilder b = new StringBuilder();
b.append("Class '")
.append(className)
.append("' is not a transformed class. Transformed classes should be in one of the following packages: ");
String sep = "";
List<String> names = new ArrayList<String>(controlledPackages);
Collections.sort(names);
for (String name : names)
{
b.append(sep);
b.append(name);
sep = ", ";
}
String message = b.append('.').toString();
throw new IllegalArgumentException(message);
}
TypeCategory getTypeCategory(String typeName)
{
synchronized (loader)
{
// TODO: Is this the right place to cache this data?
return typeName2Category.get(typeName);
}
}
@Override
public void addPlasticClassListener(PlasticClassListener listener)
{
assert listener != null;
listeners.add(listener);
}
@Override
public void removePlasticClassListener(PlasticClassListener listener)
{
assert listener != null;
listeners.remove(listener);
}
boolean isEnabled(TransformationOption option)
{
return options.contains(option);
}
void setFieldReadInstrumentation(String classInternalName, String fieldName, FieldInstrumentation fi)
{
instrumentations.get(classInternalName).read.put(fieldName, fi);
}
private FieldInstrumentations getFieldInstrumentations(String classInternalName)
{
FieldInstrumentations result = instrumentations.get(classInternalName);
if (result != null)
{
return result;
}
String className = PlasticInternalUtils.toClassName(classInternalName);
// If it is a top-level (not inner) class in a controlled package, then we
// will recursively load the class, to identify any field instrumentations
// in it.
if (!className.contains("$") && shouldInterceptClassLoading(className))
{
try
{
loadAndTransformClass(className);
// The key is written into the instrumentations map as a side-effect
// of loading the class.
return instrumentations.get(classInternalName);
} catch (Exception ex)
{
throw new RuntimeException(PlasticInternalUtils.toMessage(ex), ex);
}
}
// Either a class outside of controlled packages, or an inner class. Use a placeholder
// that contains empty maps.
result = placeholder;
instrumentations.put(classInternalName, result);
return result;
}
FieldInstrumentation getFieldInstrumentation(String ownerClassInternalName, String fieldName, boolean forRead)
{
String currentName = ownerClassInternalName;
while (true)
{
if (currentName == null)
{
return null;
}
FieldInstrumentations instrumentations = getFieldInstrumentations(currentName);
FieldInstrumentation instrumentation = instrumentations.get(fieldName, forRead);
if (instrumentation != null)
{
return instrumentation;
}
currentName = instrumentations.superClassInternalName;
}
}
void setFieldWriteInstrumentation(String classInternalName, String fieldName, FieldInstrumentation fi)
{
instrumentations.get(classInternalName).write.put(fieldName, fi);
}
boolean isCheckedException(String exceptionName)
{
Boolean cached = checkedExceptionCache.get(exceptionName);
if (cached != null)
{
return cached;
}
try
{
Class asClass = getClassLoader().loadClass(exceptionName);
boolean checked = !(Error.class.isAssignableFrom(asClass) ||
RuntimeException.class.isAssignableFrom(asClass));
checkedExceptionCache.put(exceptionName, checked);
return checked;
} catch (Exception e)
{
throw new RuntimeException(e);
}
}
}