blob: 60a2934b9b3125238d67c96240bd70b2bbd81744 [file] [log] [blame]
// Copyright 2006, 2007, 2008 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.services;
import javassist.*;
import javassist.expr.ExprEditor;
import javassist.expr.FieldAccess;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.internal.InternalComponentResources;
import org.apache.tapestry5.internal.util.MultiKey;
import org.apache.tapestry5.ioc.internal.services.CtClassSource;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.*;
import org.apache.tapestry5.ioc.internal.util.Defense;
import static org.apache.tapestry5.ioc.internal.util.Defense.notBlank;
import static org.apache.tapestry5.ioc.internal.util.Defense.notNull;
import org.apache.tapestry5.ioc.internal.util.IdAllocator;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.services.ClassFab;
import org.apache.tapestry5.ioc.services.ClassFabUtils;
import org.apache.tapestry5.ioc.services.ClassFactory;
import org.apache.tapestry5.ioc.services.MethodSignature;
import org.apache.tapestry5.ioc.util.BodyBuilder;
import org.apache.tapestry5.model.ComponentModel;
import org.apache.tapestry5.model.MutableComponentModel;
import org.apache.tapestry5.runtime.Component;
import org.apache.tapestry5.services.*;
import org.slf4j.Logger;
import static java.lang.String.format;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* Implementation of the {@link org.apache.tapestry5.internal.services.InternalClassTransformation} interface.
*/
public final class InternalClassTransformationImpl implements InternalClassTransformation
{
private static final int INIT_BUFFER_SIZE = 100;
private boolean frozen;
private final CtClass ctClass;
private final Logger logger;
private final InternalClassTransformation parentTransformation;
private final ClassPool classPool;
private final IdAllocator idAllocator;
/**
* Map, keyed on InjectKey, of field name.
*/
private final Map<MultiKey, String> injectionCache = newMap();
/**
* Map from a field to the annotation objects for that field.
*/
private Map<String, List<Annotation>> fieldAnnotations = newMap();
/**
* Used to identify fields that have been "claimed" by other annotations.
*/
private Map<String, Object> claimedFields = newMap();
private Set<String> addedFieldNames = newSet();
private Set<CtBehavior> addedMethods = newSet();
// Cache of class annotation
private List<Annotation> classAnnotations;
// Cache of method annotation
private Map<CtMethod, List<Annotation>> methodAnnotations = newMap();
private Map<CtMethod, TransformMethodSignature> methodSignatures = newMap();
private Map<TransformMethodSignature, InvocationBuilder> methodToInvocationBuilder = CollectionFactory.newMap();
// Key is field name, value is expression used to replace read access
private Map<String, String> fieldReadTransforms;
// Key is field name, value is expression used to replace read access
private Map<String, String> fieldWriteTransforms;
private Set<String> removedFieldNames;
/**
* Contains the assembled Javassist code for the class' default constructor.
*/
private StringBuilder constructor = new StringBuilder(INIT_BUFFER_SIZE);
private final List<ConstructorArg> constructorArgs;
private final ComponentModel componentModel;
private final String resourcesFieldName;
private final StringBuilder description = new StringBuilder(INIT_BUFFER_SIZE);
private Formatter formatter = new Formatter(description);
private final ClassFactory classFactory;
private final ComponentClassCache componentClassCache;
private final CtClassSource classSource;
/**
* Signature for newInstance() method of Instantiator.
*/
private static final MethodSignature NEW_INSTANCE_SIGNATURE = new MethodSignature(Component.class, "newInstance",
new Class[] {
InternalComponentResources.class },
null);
/**
* Stores transformation type data about one argument to a class constructor.
*/
static class ConstructorArg
{
private final CtClass type;
private final Object value;
/**
* Constructs new instance.
*
* @param type type of the parameter to be created (may not be null)
* @param value value to be injected via the constructor (may be null)
*/
ConstructorArg(CtClass type, Object value)
{
this.type = Defense.notNull(type, "type");
this.value = value;
}
}
/**
* This is a constructor for a base class.
*/
public InternalClassTransformationImpl(ClassFactory classFactory, CtClass ctClass,
ComponentClassCache componentClassCache,
ComponentModel componentModel, CtClassSource classSource)
{
this.ctClass = ctClass;
this.componentClassCache = componentClassCache;
this.classSource = classSource;
classPool = this.ctClass.getClassPool();
this.classFactory = classFactory;
parentTransformation = null;
this.componentModel = componentModel;
idAllocator = new IdAllocator();
logger = componentModel.getLogger();
preloadMemberNames();
constructorArgs = newList();
constructor.append("{\n");
addImplementedInterface(Component.class);
resourcesFieldName = addInjectedFieldUncached(InternalComponentResources.class, "resources", null);
TransformMethodSignature sig = new TransformMethodSignature(Modifier.PUBLIC | Modifier.FINAL,
ComponentResources.class.getName(),
"getComponentResources", null, null);
addMethod(sig, "return " + resourcesFieldName + ";");
// The "}" will be added later, inside finish().
}
/**
* Constructor for a component sub-class.
*/
private InternalClassTransformationImpl(CtClass ctClass, InternalClassTransformation parentTransformation,
ClassFactory classFactory, CtClassSource classSource,
ComponentClassCache componentClassCache,
ComponentModel componentModel)
{
this.ctClass = ctClass;
this.componentClassCache = componentClassCache;
this.classSource = classSource;
classPool = this.ctClass.getClassPool();
this.classFactory = classFactory;
logger = componentModel.getLogger();
this.parentTransformation = parentTransformation;
this.componentModel = componentModel;
resourcesFieldName = parentTransformation.getResourcesFieldName();
idAllocator = parentTransformation.getIdAllocator();
preloadMemberNames();
constructorArgs = parentTransformation.getConstructorArgs();
int count = constructorArgs.size();
// Build the call to the super-constructor.
constructor.append("{ super(");
for (int i = 1; i <= count; i++)
{
if (i > 1) constructor.append(", ");
// $0 is implicitly self, so the 0-index ConstructorArg will be Javassisst
// pseudeo-variable $1, and so forth.
constructor.append("$");
constructor.append(i);
}
constructor.append(");\n");
// The "}" will be added later, inside finish().
}
public InternalClassTransformation createChildTransformation(CtClass childClass, MutableComponentModel childModel)
{
return new InternalClassTransformationImpl(childClass, this, classFactory, classSource, componentClassCache,
childModel
);
}
private void freeze()
{
frozen = true;
// Free up stuff we don't need after freezing.
// Everything else should be final.
fieldAnnotations = null;
claimedFields = null;
addedFieldNames = null;
addedMethods = null;
classAnnotations = null;
methodAnnotations = null;
methodSignatures = null;
fieldReadTransforms = null;
fieldWriteTransforms = null;
removedFieldNames = null;
constructor = null;
formatter = null;
methodToInvocationBuilder = null;
}
public String getResourcesFieldName()
{
return resourcesFieldName;
}
/**
* Loads the names of all declared fields and methods into the idAllocator.
*/
private void preloadMemberNames()
{
addMemberNames(ctClass.getDeclaredFields());
addMemberNames(ctClass.getDeclaredMethods());
}
void verifyFields()
{
List<String> names = newList();
for (CtField field : ctClass.getDeclaredFields())
{
String name = field.getName();
if (addedFieldNames.contains(name)) continue;
int modifiers = field.getModifiers();
// Fields must be either static or private.
if (Modifier.isStatic(modifiers) || Modifier.isPrivate(modifiers)) continue;
// Groovy injects a public field named metaClass. We ignore it, and add it as a claimed
// field to prevent any of the workers from seeing it.
if (name.equals("metaClass") && getFieldType(name).equals("groovy.lang.MetaClass"))
{
claimField(name, "Ignored");
continue;
}
names.add(name);
}
if (!names.isEmpty())
throw new RuntimeException(ServicesMessages.nonPrivateFields(getClassName(), names));
}
private void addMemberNames(CtMember[] members)
{
for (CtMember member : members)
{
idAllocator.allocateId(member.getName());
}
}
public <T extends Annotation> T getFieldAnnotation(String fieldName, Class<T> annotationClass)
{
failIfFrozen();
List<Annotation> annotations = findFieldAnnotations(fieldName);
return findAnnotationInList(annotationClass, annotations);
}
public <T extends Annotation> T getMethodAnnotation(TransformMethodSignature signature, Class<T> annotationClass)
{
failIfFrozen();
CtMethod method = findMethod(signature);
if (method == null) throw new IllegalArgumentException(ServicesMessages.noDeclaredMethod(ctClass, signature));
List<Annotation> annotations = findMethodAnnotations(method);
return findAnnotationInList(annotationClass, annotations);
}
/**
* Searches an array of objects (that are really annotations instances) to find one that is of the correct type,
* which is returned.
*
* @param <T>
* @param annotationClass the annotation to search for
* @param annotations the available annotations
* @return the matching annotation instance, or null if not found
*/
private <T extends Annotation> T findAnnotationInList(Class<T> annotationClass, List<Annotation> annotations)
{
for (Object annotation : annotations)
{
if (annotationClass.isInstance(annotation)) return annotationClass.cast(annotation);
}
return null;
}
public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
{
return findAnnotationInList(annotationClass, getClassAnnotations());
}
private List<Annotation> findFieldAnnotations(String fieldName)
{
List<Annotation> annotations = fieldAnnotations.get(fieldName);
if (annotations == null)
{
annotations = findAnnotationsForField(fieldName);
fieldAnnotations.put(fieldName, annotations);
}
return annotations;
}
private List<Annotation> findMethodAnnotations(CtMethod method)
{
List<Annotation> annotations = methodAnnotations.get(method);
if (annotations == null)
{
annotations = extractAnnotations(method);
methodAnnotations.put(method, annotations);
}
return annotations;
}
private List<Annotation> findAnnotationsForField(String fieldName)
{
CtField field = findDeclaredCtField(fieldName);
return extractAnnotations(field);
}
private List<Annotation> extractAnnotations(CtMember member)
{
try
{
List<Annotation> result = newList();
addAnnotationsToList(result, member.getAnnotations());
return result;
}
catch (ClassNotFoundException ex)
{
throw new RuntimeException(ex);
}
}
private void addAnnotationsToList(List<Annotation> list, Object[] annotations)
{
for (Object o : annotations)
{
Annotation a = (Annotation) o;
list.add(a);
}
}
private CtField findDeclaredCtField(String fieldName)
{
try
{
return ctClass.getDeclaredField(fieldName);
}
catch (NotFoundException ex)
{
throw new RuntimeException(ServicesMessages.missingDeclaredField(ctClass, fieldName), ex);
}
}
public String newMemberName(String suggested)
{
failIfFrozen();
String memberName = InternalUtils.createMemberName(notBlank(suggested, "suggested"));
return idAllocator.allocateId(memberName);
}
public String newMemberName(String prefix, String baseName)
{
return newMemberName(prefix + "_" + InternalUtils.stripMemberPrefix(baseName));
}
public void addImplementedInterface(Class interfaceClass)
{
failIfFrozen();
String interfaceName = interfaceClass.getName();
try
{
CtClass ctInterface = classPool.get(interfaceName);
if (classImplementsInterface(ctInterface)) return;
implementDefaultMethodsForInterface(ctInterface);
ctClass.addInterface(ctInterface);
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
}
/**
* Adds default implementations for the methods defined by the interface (and all of its super-interfaces). The
* implementations return null (or 0, or false, as appropriate to to the method type). There are a number of
* degenerate cases that are not covered properly: these are related to base interfaces that may be implemented by
* base classes.
*
* @param ctInterface
* @throws NotFoundException
*/
private void implementDefaultMethodsForInterface(CtClass ctInterface) throws NotFoundException
{
// java.lang.Object is the parent interface of interfaces
if (ctInterface.getName().equals(Object.class.getName())) return;
for (CtMethod method : ctInterface.getDeclaredMethods())
{
addDefaultImplementation(method);
}
for (CtClass parent : ctInterface.getInterfaces())
{
implementDefaultMethodsForInterface(parent);
}
}
private void addDefaultImplementation(CtMethod method)
{
// Javassist has an oddity for interfaces: methods "inherited" from java.lang.Object show
// up as methods of the interface. We skip those and only consider the methods
// that are abstract.
if (!Modifier.isAbstract(method.getModifiers())) return;
try
{
CtMethod newMethod = CtNewMethod.copy(method, ctClass, null);
// Methods from interfaces are always public. We definitely
// need to change the modifiers of the method so that
// it is not abstract.
newMethod.setModifiers(Modifier.PUBLIC);
// Javassist will provide a minimal implementation for us (return null, false, 0,
// whatever).
newMethod.setBody(null);
ctClass.addMethod(newMethod);
TransformMethodSignature sig = getMethodSignature(newMethod);
addMethodToDescription("add default", sig, "<default>");
}
catch (CannotCompileException ex)
{
throw new RuntimeException(ServicesMessages.errorAddingMethod(ctClass, method
.getName(), ex), ex);
}
}
/**
* Check to see if the target class (or any of its super classes) implements the provided interface. This is geared
* for simple interfaces (that don't extend other interfaces), thus if the class (or a base class) implement
* interface Y that extends interface X, we may not return true for interface X.
*/
private boolean classImplementsInterface(CtClass ctInterface) throws NotFoundException
{
for (CtClass current = ctClass; current != null; current = current.getSuperclass())
{
for (CtClass anInterface : current.getInterfaces())
{
if (anInterface == ctInterface) return true;
}
}
return false;
}
public void claimField(String fieldName, Object tag)
{
notBlank(fieldName, "fieldName");
notNull(tag, "tag");
failIfFrozen();
Object existing = claimedFields.get(fieldName);
if (existing != null)
{
String message = ServicesMessages.fieldAlreadyClaimed(fieldName, ctClass, existing, tag);
throw new RuntimeException(message);
}
// TODO: Ensure that fieldName is a known field?
claimedFields.put(fieldName, tag);
}
public void addMethod(TransformMethodSignature signature, String methodBody)
{
addOrReplaceMethod(signature, methodBody, true);
}
private void addOrReplaceMethod(TransformMethodSignature signature, String methodBody, boolean addAsNew)
{
failIfFrozen();
CtClass returnType = findCtClass(signature.getReturnType());
CtClass[] parameters = buildCtClassList(signature.getParameterTypes());
CtClass[] exceptions = buildCtClassList(signature.getExceptionTypes());
String suffix = addAsNew ? "" : " transformed";
String action = "add" + suffix;
try
{
CtMethod existing = ctClass.getDeclaredMethod(signature.getMethodName(), parameters);
if (existing != null)
{
action = "replace" + suffix;
ctClass.removeMethod(existing);
}
}
catch (NotFoundException ex)
{
// That's ok. Kind of sloppy to rely on a thrown exception; wish getDeclaredMethod()
// would return null for
// that case. Alternately, we could maintain a set of the method signatures of declared
// or added methods.
}
try
{
CtMethod method = new CtMethod(returnType, signature.getMethodName(), parameters, ctClass);
// TODO: Check for duplicate method add
method.setModifiers(signature.getModifiers());
method.setBody(methodBody);
method.setExceptionTypes(exceptions);
ctClass.addMethod(method);
if (addAsNew) addedMethods.add(method);
}
catch (CannotCompileException ex)
{
throw new MethodCompileException(ServicesMessages.methodCompileError(signature, methodBody, ex), methodBody,
ex);
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
addMethodToDescription(action, signature, methodBody);
}
public void addTransformedMethod(TransformMethodSignature methodSignature, String methodBody)
{
addOrReplaceMethod(methodSignature, methodBody, false);
}
private CtClass[] buildCtClassList(String[] typeNames)
{
CtClass[] result = new CtClass[typeNames.length];
for (int i = 0; i < typeNames.length; i++)
result[i] = findCtClass(typeNames[i]);
return result;
}
private CtClass findCtClass(String type)
{
try
{
return classPool.get(type);
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
}
public void extendMethod(TransformMethodSignature methodSignature, String methodBody)
{
failIfFrozen();
CtMethod method = findMethod(methodSignature);
try
{
method.insertAfter(methodBody);
}
catch (CannotCompileException ex)
{
throw new MethodCompileException(ServicesMessages.methodCompileError(methodSignature, methodBody, ex),
methodBody, ex);
}
addMethodToDescription("extend", methodSignature, methodBody);
addedMethods.add(method);
}
public void extendExistingMethod(TransformMethodSignature methodSignature, String methodBody)
{
failIfFrozen();
CtMethod method = findMethod(methodSignature);
try
{
method.insertAfter(methodBody);
}
catch (CannotCompileException ex)
{
throw new MethodCompileException(ServicesMessages.methodCompileError(methodSignature, methodBody, ex),
methodBody, ex);
}
addMethodToDescription("extend existing", methodSignature, methodBody);
}
public void copyMethod(TransformMethodSignature sourceMethod, int modifiers, String newMethodName)
{
failIfFrozen();
CtClass returnType = findCtClass(sourceMethod.getReturnType());
CtClass[] parameters = buildCtClassList(sourceMethod.getParameterTypes());
CtClass[] exceptions = buildCtClassList(sourceMethod.getExceptionTypes());
CtMethod source = findMethod(sourceMethod);
try
{
CtMethod method = new CtMethod(returnType, newMethodName, parameters, ctClass);
method.setModifiers(modifiers);
method.setExceptionTypes(exceptions);
method.setBody(source, null);
ctClass.addMethod(method);
}
catch (CannotCompileException ex)
{
throw new RuntimeException(String.format("Error copying method %s to new method %s().",
sourceMethod,
newMethodName), ex);
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
// The new method is *not* considered an added method, so field references inside the method
// will be transformed.
formatter.format("\n%s renamed to %s\n\n", sourceMethod, newMethodName);
}
public void addCatch(TransformMethodSignature methodSignature, String exceptionType, String body)
{
failIfFrozen();
CtMethod method = findMethod(methodSignature);
CtClass exceptionCtType = findCtClass(exceptionType);
try
{
method.addCatch(body, exceptionCtType);
}
catch (CannotCompileException ex)
{
throw new MethodCompileException(ServicesMessages.methodCompileError(methodSignature, body, ex),
body, ex);
}
addMethodToDescription(String.format("catch(%s) in", exceptionType), methodSignature, body);
}
public void prefixMethod(TransformMethodSignature methodSignature, String methodBody)
{
failIfFrozen();
CtMethod method = findMethod(methodSignature);
try
{
method.insertBefore(methodBody);
}
catch (CannotCompileException ex)
{
throw new MethodCompileException(ServicesMessages.methodCompileError(methodSignature, methodBody, ex),
methodBody, ex);
}
addMethodToDescription("prefix", methodSignature, methodBody);
}
private void addMethodToDescription(String operation, TransformMethodSignature methodSignature, String methodBody)
{
formatter.format("%s method: %s %s %s(", operation, Modifier.toString(methodSignature
.getModifiers()), methodSignature.getReturnType(), methodSignature.getMethodName());
String[] parameterTypes = methodSignature.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++)
{
if (i > 0) description.append(", ");
formatter.format("%s $%d", parameterTypes[i], i + 1);
}
description.append(")");
String[] exceptionTypes = methodSignature.getExceptionTypes();
for (int i = 0; i < exceptionTypes.length; i++)
{
if (i == 0) description.append("\n throws ");
else description.append(", ");
description.append(exceptionTypes[i]);
}
formatter.format("\n%s\n\n", methodBody);
}
private CtMethod findMethod(TransformMethodSignature methodSignature)
{
CtMethod method = findDeclaredMethod(methodSignature);
if (method != null) return method;
CtMethod result = addOverrideOfSuperclassMethod(methodSignature);
if (result != null) return result;
throw new IllegalArgumentException(ServicesMessages.noDeclaredMethod(ctClass, methodSignature));
}
private CtMethod findDeclaredMethod(TransformMethodSignature methodSignature)
{
for (CtMethod method : ctClass.getDeclaredMethods())
{
if (match(method, methodSignature)) return method;
}
return null;
}
private CtMethod addOverrideOfSuperclassMethod(TransformMethodSignature methodSignature)
{
try
{
for (CtClass current = ctClass; current != null; current = current.getSuperclass())
{
for (CtMethod method : current.getDeclaredMethods())
{
if (match(method, methodSignature))
{
// TODO: If the moethod is not overridable (i.e. private, or final)?
// Perhaps we should limit it to just public methods.
CtMethod newMethod = CtNewMethod.delegator(method, ctClass);
ctClass.addMethod(newMethod);
return newMethod;
}
}
}
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
catch (CannotCompileException ex)
{
throw new RuntimeException(ex);
}
// Not found in a super-class.
return null;
}
private boolean match(CtMethod method, TransformMethodSignature sig)
{
if (!sig.getMethodName().equals(method.getName())) return false;
CtClass[] paramTypes;
try
{
paramTypes = method.getParameterTypes();
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
String[] sigTypes = sig.getParameterTypes();
int count = sigTypes.length;
if (paramTypes.length != count) return false;
for (int i = 0; i < count; i++)
{
String paramType = paramTypes[i].getName();
if (!paramType.equals(sigTypes[i])) return false;
}
// Ignore exceptions thrown and modifiers.
// TODO: Validate a match on return type?
return true;
}
public List<String> findFieldsWithAnnotation(final Class<? extends Annotation> annotationClass)
{
return searchFieldsWithAnnotation(annotationClass, true);
}
public List<String> findAllFieldsWithAnnotation(Class<? extends Annotation> annotationClass)
{
return searchFieldsWithAnnotation(annotationClass, false);
}
private List<String> searchFieldsWithAnnotation(final Class<? extends Annotation> annotationClass,
boolean skipClaimedFields)
{
FieldFilter filter = new FieldFilter()
{
public boolean accept(String fieldName, String fieldType)
{
return getFieldAnnotation(fieldName, annotationClass) != null;
}
};
return searchFieldsAndFilter(filter, skipClaimedFields);
}
public List<String> findFields(FieldFilter filter)
{
return searchFieldsAndFilter(filter, true);
}
private List<String> searchFieldsAndFilter(FieldFilter filter, boolean skipClaimedFields)
{
failIfFrozen();
List<String> result = newList();
try
{
for (CtField field : ctClass.getDeclaredFields())
{
if (!isInstanceField(field)) continue;
String fieldName = field.getName();
if (skipClaimedFields && claimedFields.containsKey(fieldName)) continue;
if (filter.accept(fieldName, field.getType().getName())) result.add(fieldName);
}
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
Collections.sort(result);
return result;
}
public List<TransformMethodSignature> findMethodsWithAnnotation(Class<? extends Annotation> annotationClass)
{
failIfFrozen();
List<TransformMethodSignature> result = newList();
for (CtMethod method : ctClass.getDeclaredMethods())
{
List<Annotation> annotations = findMethodAnnotations(method);
if (findAnnotationInList(annotationClass, annotations) != null)
{
TransformMethodSignature sig = getMethodSignature(method);
result.add(sig);
}
}
Collections.sort(result);
return result;
}
public List<TransformMethodSignature> findMethods(MethodFilter filter)
{
notNull(filter, "filter");
List<TransformMethodSignature> result = newList();
for (CtMethod method : ctClass.getDeclaredMethods())
{
TransformMethodSignature sig = getMethodSignature(method);
if (filter.accept(sig)) result.add(sig);
}
Collections.sort(result);
return result;
}
private TransformMethodSignature getMethodSignature(CtMethod method)
{
TransformMethodSignature result = methodSignatures.get(method);
if (result == null)
{
try
{
String type = method.getReturnType().getName();
String[] parameters = toTypeNames(method.getParameterTypes());
String[] exceptions = toTypeNames(method.getExceptionTypes());
result = new TransformMethodSignature(method.getModifiers(), type, method.getName(), parameters,
exceptions);
methodSignatures.put(method, result);
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
}
return result;
}
private String[] toTypeNames(CtClass[] types)
{
String[] result = new String[types.length];
for (int i = 0; i < types.length; i++)
result[i] = types[i].getName();
return result;
}
public List<String> findUnclaimedFields()
{
failIfFrozen();
List<String> names = newList();
Set<String> skipped = newSet();
skipped.addAll(claimedFields.keySet());
skipped.addAll(addedFieldNames);
if (removedFieldNames != null) skipped.addAll(removedFieldNames);
for (CtField field : ctClass.getDeclaredFields())
{
if (!isInstanceField(field)) continue;
String name = field.getName();
if (skipped.contains(name)) continue;
// May need to add a filter to edit out explicitly added fields.
names.add(name);
}
Collections.sort(names);
return names;
}
private boolean isInstanceField(CtField field)
{
int modifiers = field.getModifiers();
return Modifier.isPrivate(modifiers) && !Modifier.isStatic(modifiers);
}
public String getFieldType(String fieldName)
{
failIfFrozen();
CtClass type = getFieldCtType(fieldName);
return type.getName();
}
public boolean isField(String fieldName)
{
failIfFrozen();
try
{
CtField field = ctClass.getDeclaredField(fieldName);
return isInstanceField(field);
}
catch (NotFoundException ex)
{
return false;
}
}
public int getFieldModifiers(String fieldName)
{
failIfFrozen();
try
{
return ctClass.getDeclaredField(fieldName).getModifiers();
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
}
private CtClass getFieldCtType(String fieldName)
{
try
{
CtField field = ctClass.getDeclaredField(fieldName);
return field.getType();
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
}
public String addField(int modifiers, String type, String suggestedName)
{
failIfFrozen();
String fieldName = newMemberName(suggestedName);
try
{
CtClass ctType = convertNameToCtType(type);
CtField field = new CtField(ctType, fieldName, ctClass);
field.setModifiers(modifiers);
ctClass.addField(field);
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
catch (CannotCompileException ex)
{
throw new RuntimeException(ex);
}
formatter
.format("add field: %s %s %s;\n\n", Modifier.toString(modifiers), type, fieldName);
addedFieldNames.add(fieldName);
return fieldName;
}
public String addInjectedField(Class type, String suggestedName, Object value)
{
notNull(type, "type");
failIfFrozen();
MultiKey key = new MultiKey(type, value);
String fieldName = searchForPreviousInjection(key);
if (fieldName != null) return fieldName;
// TODO: Probably doesn't handle arrays and primitives.
fieldName = addInjectedFieldUncached(type, suggestedName, value);
// Remember the injection in-case this class, or a subclass, injects the value again.
injectionCache.put(key, fieldName);
return fieldName;
}
/**
* This is split out from {@link #addInjectedField(Class, String, Object)} to handle a special case for the
* InternalComponentResources, which is null when "injected" (during the class transformation) and is only
* determined when a component is actually instantiated.
*/
private String addInjectedFieldUncached(Class type, String suggestedName, Object value)
{
CtClass ctType;
try
{
ctType = classPool.get(type.getName());
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
String fieldName = addField(Modifier.PROTECTED | Modifier.FINAL, type.getName(), suggestedName);
addInjectToConstructor(fieldName, ctType, value);
addedFieldNames.add(fieldName);
return fieldName;
}
public String searchForPreviousInjection(MultiKey key)
{
String result = injectionCache.get(key);
if (result != null) return result;
if (parentTransformation != null) return parentTransformation.searchForPreviousInjection(key);
return null;
}
public void advise(TransformMethodSignature methodSignature, ComponentMethodAdvice advice)
{
Defense.notNull(methodSignature, "methodSignature");
Defense.notNull(advice, "advice");
InvocationBuilder builder = methodToInvocationBuilder.get(methodSignature);
if (builder == null)
{
builder = new InvocationBuilder(this, componentClassCache, methodSignature, classSource);
methodToInvocationBuilder.put(methodSignature, builder);
}
builder.addAdvice(advice);
}
/**
* Adds a parameter to the constructor for the class; the parameter is used to initialize the value for a field.
*
* @param fieldName name of field to inject
* @param fieldType Javassist type of the field (and corresponding parameter)
* @param value the value to be injected (which will in unusual cases be null)
*/
private void addInjectToConstructor(String fieldName, CtClass fieldType, Object value)
{
constructorArgs.add(new ConstructorArg(fieldType, value));
extendConstructor(format(" %s = $%d;", fieldName, constructorArgs.size()));
}
public void injectField(String fieldName, Object value)
{
notNull(fieldName, "fieldName");
failIfFrozen();
CtClass type = getFieldCtType(fieldName);
addInjectToConstructor(fieldName, type, value);
makeReadOnly(fieldName);
}
private CtClass convertNameToCtType(String type) throws NotFoundException
{
return classPool.get(type);
}
public void finish()
{
failIfFrozen();
for (InvocationBuilder builder : methodToInvocationBuilder.values())
{
builder.commit();
}
performFieldTransformations();
addConstructor();
verifyFields();
freeze();
}
private void addConstructor()
{
String initializer = idAllocator.allocateId("initializer");
try
{
CtConstructor defaultConstructor = ctClass.getConstructor("()V");
CtMethod initializerMethod = defaultConstructor.toMethod(initializer, ctClass);
ctClass.addMethod(initializerMethod);
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
formatter.format("convert default constructor: %s();\n\n", initializer);
int count = constructorArgs.size();
CtClass[] types = new CtClass[count];
for (int i = 0; i < count; i++)
{
ConstructorArg arg = constructorArgs.get(i);
types[i] = arg.type;
}
// Add a call to the initializer; the method converted fromt the classes default
// constructor.
constructor.append(" ");
constructor.append(initializer);
// This finally matches the "{" added inside the constructor
constructor.append("();\n\n}");
String constructorBody = constructor.toString();
try
{
CtConstructor cons = CtNewConstructor.make(types, null, constructorBody, ctClass);
ctClass.addConstructor(cons);
}
catch (CannotCompileException ex)
{
throw new RuntimeException(ex);
}
formatter.format("add constructor: %s(", ctClass.getName());
for (int i = 0; i < count; i++)
{
if (i > 0) description.append(", ");
formatter.format("%s $%d", types[i].getName(), i + 1);
}
formatter.format(")\n%s\n\n", constructorBody);
}
public Instantiator createInstantiator()
{
String componentClassName = ctClass.getName();
String name = ClassFabUtils.generateClassName("Instantiator");
ClassFab cf = classFactory.newClass(name, AbstractInstantiator.class);
BodyBuilder constructor = new BodyBuilder();
// This is realy -1 + 2: The first value in constructorArgs is the InternalComponentResources, which doesn't
// count toward's the Instantiator's constructor ... then we add in the Model and String description.
// It's tricky because there's the constructor parameters for the Instantiator, most of which are stored
// in fields and then used as the constructor parameters for the Component.
Class[] constructorParameterTypes = new Class[constructorArgs.size() + 1];
Object[] constructorParameterValues = new Object[constructorArgs.size() + 1];
constructorParameterTypes[0] = ComponentModel.class;
constructorParameterValues[0] = componentModel;
constructorParameterTypes[1] = String.class;
constructorParameterValues[1] = String.format("Instantiator[%s]", componentClassName);
BodyBuilder newInstance = new BodyBuilder();
newInstance.add("return new %s($1", componentClassName);
constructor.begin();
// Pass the model and description to AbstractInstantiator
constructor.addln("super($1, $2);");
// Again, skip the (implicit) InternalComponentResources field, that's
// supplied to the Instantiator's newInstance() method.
for (int i = 1; i < constructorArgs.size(); i++)
{
ConstructorArg arg = constructorArgs.get(i);
CtClass argCtType = arg.type;
Class argType = toClass(argCtType.getName());
boolean primitive = argCtType.isPrimitive();
Class fieldType = primitive ? ClassFabUtils.getPrimitiveType(argType) : argType;
String fieldName = "_param_" + i;
constructorParameterTypes[i + 1] = argType;
constructorParameterValues[i + 1] = arg.value;
cf.addField(fieldName, fieldType);
// $1 is model, $2 is description, to $3 is first dynamic parameter.
// The arguments may be wrapper types, so we cast down to
// the primitive type.
String parameterReference = "$" + (i + 2);
constructor.addln("%s = %s;",
fieldName,
ClassFabUtils.castReference(parameterReference, fieldType.getName()));
newInstance.add(", %s", fieldName);
}
constructor.end();
newInstance.addln(");");
cf.addConstructor(constructorParameterTypes, null, constructor.toString());
cf.addMethod(Modifier.PUBLIC, NEW_INSTANCE_SIGNATURE, newInstance.toString());
Class instantiatorClass = cf.createClass();
try
{
Object instance = instantiatorClass.getConstructors()[0].newInstance(constructorParameterValues);
return (Instantiator) instance;
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
}
private void failIfFrozen()
{
if (frozen) throw new IllegalStateException(
"The ClassTransformation instance (for " + ctClass.getName() + ") has completed all transformations and may not be further modified.");
}
private void failIfNotFrozen()
{
if (!frozen) throw new IllegalStateException(
"The ClassTransformation instance (for " + ctClass.getName() + ") has not yet completed all transformations.");
}
public IdAllocator getIdAllocator()
{
failIfNotFrozen();
return idAllocator;
}
public List<ConstructorArg> getConstructorArgs()
{
failIfNotFrozen();
return CollectionFactory.newList(constructorArgs);
}
public List<Annotation> getClassAnnotations()
{
failIfFrozen();
if (classAnnotations == null) assembleClassAnnotations();
return classAnnotations;
}
private void assembleClassAnnotations()
{
classAnnotations = newList();
try
{
for (CtClass current = ctClass; current != null; current = current.getSuperclass())
{
addAnnotationsToList(classAnnotations, current.getAnnotations());
}
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
catch (ClassNotFoundException ex)
{
throw new RuntimeException(ex);
}
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder("InternalClassTransformation[\n");
try
{
Formatter formatter = new Formatter(builder);
formatter.format("%s %s extends %s", Modifier.toString(ctClass.getModifiers()), ctClass.getName(),
ctClass.getSuperclass().getName());
CtClass[] interfaces = ctClass.getInterfaces();
for (int i = 0; i < interfaces.length; i++)
{
if (i == 0) builder.append("\n implements ");
else builder.append(", ");
builder.append(interfaces[i].getName());
}
formatter.format("\n\n%s", description.toString());
}
catch (NotFoundException ex)
{
builder.append(ex);
}
builder.append("]");
return builder.toString();
}
public void makeReadOnly(String fieldName)
{
String methodName = newMemberName("write", fieldName);
String fieldType = getFieldType(fieldName);
TransformMethodSignature sig = new TransformMethodSignature(Modifier.PRIVATE, "void", methodName,
new String[] { fieldType }, null);
String message = ServicesMessages.readOnlyField(ctClass.getName(), fieldName);
String body = format("throw new java.lang.RuntimeException(\"%s\");", message);
addMethod(sig, body);
replaceWriteAccess(fieldName, methodName);
}
public void removeField(String fieldName)
{
formatter.format("remove field %s;\n\n", fieldName);
// TODO: We could check that there's an existing field read and field write transform ...
if (removedFieldNames == null) removedFieldNames = newSet();
removedFieldNames.add(fieldName);
}
public void replaceReadAccess(String fieldName, String methodName)
{
// Explicitly reference $0 (aka "this") because of TAPESTRY-1511.
// $0 is valid even inside a static method.
String body = String.format("$_ = $0.%s();", methodName);
if (fieldReadTransforms == null) fieldReadTransforms = newMap();
// TODO: Collisions?
fieldReadTransforms.put(fieldName, body);
formatter.format("replace read %s: %s();\n\n", fieldName, methodName);
}
public void replaceWriteAccess(String fieldName, String methodName)
{
// Explicitly reference $0 (aka "this") because of TAPESTRY-1511.
// $0 is valid even inside a static method.
String body = String.format("$0.%s($1);", methodName);
if (fieldWriteTransforms == null) fieldWriteTransforms = newMap();
// TODO: Collisions?
fieldWriteTransforms.put(fieldName, body);
formatter.format("replace write %s: %s();\n\n", fieldName, methodName);
}
private void performFieldTransformations()
{
// If no field transformations have been requested, then we can save ourselves some
// trouble!
if (fieldReadTransforms != null || fieldWriteTransforms != null) replaceFieldAccess();
if (removedFieldNames != null)
{
for (String fieldName : removedFieldNames)
{
try
{
CtField field = ctClass.getDeclaredField(fieldName);
ctClass.removeField(field);
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
}
}
}
static final int SYNTHETIC = 0x00001000;
private void replaceFieldAccess()
{
// Provide empty maps here, to make the code in the inner class a tad
// easier.
if (fieldReadTransforms == null) fieldReadTransforms = newMap();
if (fieldWriteTransforms == null) fieldWriteTransforms = newMap();
ExprEditor editor = new ExprEditor()
{
@Override
public void edit(FieldAccess access) throws CannotCompileException
{
CtBehavior where = access.where();
if (where instanceof CtConstructor) return;
boolean isRead = access.isReader();
String fieldName = access.getFieldName();
CtMethod method = (CtMethod) where;
formatter.format("Checking field %s %s in method %s(): ", isRead ? "read" : "write", fieldName,
method.getName());
// Ignore any methods to were added as part of the transformation.
// If we reference the field there, we really mean the field.
if (addedMethods.contains(where))
{
formatter.format("added method\n");
return;
}
Map<String, String> transformMap = isRead ? fieldReadTransforms : fieldWriteTransforms;
String body = transformMap.get(fieldName);
if (body == null)
{
formatter.format("field not transformed\n");
return;
}
formatter.format("replacing with %s\n", body);
access.replace(body);
}
};
try
{
ctClass.instrument(editor);
}
catch (CannotCompileException ex)
{
throw new RuntimeException(ex);
}
formatter.format("\n");
}
public Class toClass(String type)
{
String finalType = TransformUtils.getWrapperTypeName(type);
try
{
return Class.forName(finalType, true, classFactory.getClassLoader());
}
catch (ClassNotFoundException ex)
{
throw new RuntimeException(ex);
}
}
public String getClassName()
{
return ctClass.getName();
}
public Logger getLogger()
{
return logger;
}
public void extendConstructor(String statement)
{
notNull(statement, "statement");
failIfFrozen();
constructor.append(statement);
constructor.append("\n");
}
public String getMethodIdentifier(TransformMethodSignature signature)
{
notNull(signature, "signature");
CtMethod method = findMethod(signature);
int lineNumber = method.getMethodInfo2().getLineNumber(0);
CtClass enclosingClass = method.getDeclaringClass();
String sourceFile = enclosingClass.getClassFile2().getSourceFile();
return format("%s.%s (at %s:%d)", enclosingClass.getName(), signature
.getMediumDescription(), sourceFile, lineNumber);
}
public boolean isRootTransformation()
{
return parentTransformation == null;
}
}