| // Copyright 2006, 2007, 2008, 2009, 2010, 2011 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 java.lang.annotation.Annotation; |
| import java.lang.annotation.Inherited; |
| import java.lang.reflect.Modifier; |
| import java.util.Arrays; |
| import java.util.Formatter; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import javassist.*; |
| import javassist.expr.ExprEditor; |
| import javassist.expr.FieldAccess; |
| |
| import org.apache.tapestry5.ComponentResources; |
| import org.apache.tapestry5.func.Predicate; |
| import org.apache.tapestry5.internal.InternalComponentResources; |
| import org.apache.tapestry5.ioc.internal.services.CtClassSource; |
| import org.apache.tapestry5.ioc.internal.util.CollectionFactory; |
| 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.FieldValueConduit; |
| import org.apache.tapestry5.ioc.services.MethodSignature; |
| import org.apache.tapestry5.ioc.util.BodyBuilder; |
| import org.apache.tapestry5.ioc.util.IdAllocator; |
| import org.apache.tapestry5.model.ComponentModel; |
| import org.apache.tapestry5.model.MutableComponentModel; |
| import org.apache.tapestry5.runtime.Component; |
| import org.apache.tapestry5.runtime.ComponentEvent; |
| import org.apache.tapestry5.services.*; |
| import org.slf4j.Logger; |
| |
| /** |
| * Implementation of the {@link org.apache.tapestry5.internal.services.InternalClassTransformation} interface. |
| */ |
| @SuppressWarnings("all") |
| public final class InternalClassTransformationImpl implements InternalClassTransformation |
| { |
| public static final MethodSignature INVOKE_SIGNATURE = new MethodSignature(MethodInvocationResult.class, "invoke", |
| new Class[] |
| { Object.class, Object[].class }, null); |
| |
| public static final MethodSignature FIELD_ACCESS_READ_SIGNATURE = new MethodSignature(Object.class, "read", |
| new Class[] |
| { Object.class }, null); |
| |
| public static final MethodSignature FIELD_ACCESS_WRITE_SIGNATURE = new MethodSignature(void.class, "write", |
| new Class[] |
| { Object.class, Object.class }, null); |
| |
| 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; |
| |
| private final CtClass providerType; |
| |
| class TransformMethodImpl implements TransformMethod |
| { |
| final CtMethod method; |
| |
| private final TransformMethodSignature sig; |
| |
| private List<Annotation> annotations; |
| |
| private final boolean added; |
| |
| private ComponentMethodInvocationBuilder builder; |
| |
| private MethodAccess access; |
| |
| private String identifier; |
| |
| private Boolean override; |
| |
| private List<List<Annotation>> parameterAnnotations; |
| |
| TransformMethodImpl(CtMethod method, boolean added) |
| { |
| this.method = method; |
| this.sig = toMethodSignature(method); |
| this.added = added; |
| |
| } |
| |
| @Override |
| public String toString() |
| { |
| return String.format("TransformMethod[%s]", getMethodIdentifier()); |
| } |
| |
| public int compareTo(TransformMethod o) |
| { |
| return sig.compareTo(o.getSignature()); |
| } |
| |
| public <T extends Annotation> T getAnnotation(Class<T> annotationClass) |
| { |
| if (annotations == null) |
| annotations = extractAnnotations(method); |
| |
| return findAnnotationInList(annotationClass, annotations); |
| } |
| |
| public <A extends Annotation> A getParameterAnnotation(int index, Class<A> annotationType) |
| { |
| if (parameterAnnotations == null) |
| extractParameterAnnotations(); |
| |
| return findAnnotationInList(annotationType, parameterAnnotations.get(index)); |
| } |
| |
| private void extractParameterAnnotations() |
| { |
| int count = sig.getParameterTypes().length; |
| |
| parameterAnnotations = CollectionFactory.newList(); |
| |
| for (int parameterIndex = 0; parameterIndex < count; parameterIndex++) |
| { |
| List<Annotation> annotations = extractAnnotationsForParameter(parameterIndex); |
| |
| parameterAnnotations.add(annotations); |
| } |
| } |
| |
| private List<Annotation> extractAnnotationsForParameter(int parameterIndex) |
| { |
| List<Annotation> result = CollectionFactory.newList(); |
| |
| Object[] parameterAnnotations = method.getAvailableParameterAnnotations()[parameterIndex]; |
| |
| addAnnotationsToList(result, parameterAnnotations, false); |
| |
| return result; |
| } |
| |
| public TransformMethodSignature getSignature() |
| { |
| return sig; |
| } |
| |
| public String getName() |
| { |
| return sig.getMethodName(); |
| } |
| |
| public void addAdvice(ComponentMethodAdvice advice) |
| { |
| failIfFrozen(); |
| |
| assert advice != null; |
| |
| if (builder == null) |
| builder = createBuilder(sig); |
| |
| builder.addAdvice(advice); |
| |
| formatter.format("add advice %s : %s\n\n", sig.getMediumDescription(), advice); |
| } |
| |
| public void addOperationAfter(ComponentInstanceOperation operation) |
| { |
| addAdvice(toAfterAdvice(operation)); |
| } |
| |
| public void addOperationBefore(ComponentInstanceOperation operation) |
| { |
| addAdvice(toBeforeAdvice(operation)); |
| } |
| |
| public MethodAccess getAccess() |
| { |
| failIfFrozen(); |
| |
| if (access == null) |
| access = createMethodAccess(); |
| |
| return access; |
| } |
| |
| private MethodAccess createMethodAccess() |
| { |
| if (isPrivate()) |
| return createPrivateMethodAccess(); |
| |
| return createNonPrivateMethodAccess(); |
| } |
| |
| private boolean isPrivate() |
| { |
| return Modifier.isPrivate(sig.getModifiers()); |
| } |
| |
| private MethodAccess createNonPrivateMethodAccess() |
| { |
| // For a public method, given the instance, we can just invoke the method directly |
| // from the MethodAccess object. |
| |
| String accessTarget = "instance." + sig.getMethodName(); |
| |
| return createMethodAccessForTarget(accessTarget, false); |
| } |
| |
| private MethodAccess createMethodAccessForTarget(String accessTarget, boolean passInstance) |
| { |
| boolean isVoid = sig.getReturnType().equals("void"); |
| |
| BodyBuilder builder = new BodyBuilder().begin(); |
| |
| builder.addln("%s instance = (%<s) $1;", getClassName()); |
| |
| builder.addln("try").begin(); |
| |
| if (!isVoid) |
| { |
| builder.add("return success(($w) "); |
| } |
| |
| // Call the target, even if the eventual method is void |
| |
| builder.add(accessTarget); |
| builder.add("("); |
| |
| if (passInstance) |
| builder.add("instance"); |
| |
| int p = 0; |
| |
| for (String type : sig.getParameterTypes()) |
| { |
| if (passInstance || p != 0) |
| builder.add(", "); |
| |
| String ref = String.format("$2[%d]", p++); |
| builder.add(ClassFabUtils.castReference(ref, type)); |
| } |
| |
| // Balance the call to success() |
| if (!isVoid) |
| builder.add(")"); |
| |
| builder.addln(");"); |
| |
| if (isVoid) |
| builder.addln("return success(null);"); |
| |
| builder.end(); // try |
| builder.addln("catch (java.lang.RuntimeException ex) { throw ex; }"); |
| builder.addln("catch (java.lang.Exception ex) { return fail(ex); }"); |
| |
| builder.end(); |
| |
| return instantiateMethodAccessFromBody(builder.toString()); |
| } |
| |
| private MethodAccess instantiateMethodAccessFromBody(String body) |
| { |
| // The access object is created in the same package as the component, so that it can access |
| // protected and package private methods. |
| String accessClassName = String.format("%s$MethodAccess_%s_%s", getClassName(), sig.getMethodName(), |
| ClassFabUtils.nextUID()); |
| |
| ClassFab cf = classFactory.newClass(accessClassName, AbstractMethodAccess.class); |
| |
| cf.addMethod(Modifier.PUBLIC, INVOKE_SIGNATURE, body); |
| |
| cf.addToString(String.format("MethodAccess[method %s of class %s]", sig.getMediumDescription(), |
| getClassName())); |
| |
| Class accessClass = cf.createClass(); |
| |
| try |
| { |
| Object accessInstance = accessClass.newInstance(); |
| |
| return (MethodAccess) accessInstance; |
| } |
| catch (Exception ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| private MethodAccess createPrivateMethodAccess() |
| { |
| // As with Java inner classes, we have to create a static bridge method. |
| |
| String staticAccessMethodName = createStaticAccessMethodForNonPublicMethod(); |
| |
| // Have the MethodAccess object call the static method and pass the |
| // instance object as the first parameter. The static method will then |
| // invoke the non-public method on the passed instance. |
| |
| return createMethodAccessForTarget(String.format("%s#%s", getClassName(), staticAccessMethodName), true); |
| } |
| |
| /** |
| * The static method takes the same parameters as the main method, but takes |
| * an instance object first. Invoking the static method turns into an invocation |
| * of the proper method of the instance object. |
| * |
| * @return the name of the created static access method |
| */ |
| private String createStaticAccessMethodForNonPublicMethod() |
| { |
| List<String> parameterTypes = CollectionFactory.newList(getClassName()); |
| parameterTypes.addAll(Arrays.asList(sig.getParameterTypes())); |
| |
| String methodName = newMemberName("access", sig.getMethodName()); |
| |
| TransformMethodSignature accessMethodSignature = new TransformMethodSignature(Modifier.PUBLIC |
| + Modifier.STATIC, sig.getReturnType(), methodName, parameterTypes.toArray(new String[0]), |
| sig.getExceptionTypes()); |
| |
| boolean isVoid = sig.getReturnType().equals("void"); |
| |
| BodyBuilder builder = new BodyBuilder(); |
| |
| builder.begin(); |
| |
| if (!isVoid) |
| builder.add("return "); |
| |
| builder.add("$1.%s(", sig.getMethodName()); |
| |
| for (int i = 0; i < sig.getParameterTypes().length; i++) |
| { |
| if (i > 0) |
| builder.add(", "); |
| |
| builder.add("$%d", i + 2); |
| } |
| |
| builder.addln(");"); |
| |
| builder.end(); |
| |
| addNewMethod(accessMethodSignature, builder.toString()); |
| |
| return methodName; |
| } |
| |
| public String getMethodIdentifier() |
| { |
| if (identifier == null) |
| { |
| int lineNumber = method.getMethodInfo2().getLineNumber(0); |
| CtClass enclosingClass = method.getDeclaringClass(); |
| String sourceFile = enclosingClass.getClassFile2().getSourceFile(); |
| |
| identifier = String.format("%s.%s (at %s:%d)", enclosingClass.getName(), sig.getMediumDescription(), |
| sourceFile, lineNumber); |
| } |
| |
| return identifier; |
| } |
| |
| public boolean isOverride() |
| { |
| if (override == null) |
| override = searchForOverride(); |
| |
| return override; |
| } |
| |
| private boolean searchForOverride() |
| { |
| InternalClassTransformation search = parentTransformation; |
| |
| while (search != null) |
| { |
| if (search.isMethod(sig)) |
| return true; |
| |
| search = search.getParentTransformation(); |
| } |
| |
| // Not found in any super-class. |
| |
| return false; |
| } |
| |
| void doFinish() |
| { |
| if (builder != null) |
| { |
| builder.commit(); |
| builder = null; |
| } |
| } |
| } |
| |
| class TransformFieldImpl implements TransformField |
| { |
| private final CtField field; |
| |
| private final CtClass fieldType; |
| |
| private final String name, type; |
| |
| private boolean added; |
| |
| private List<Annotation> annotations; |
| |
| private Object claimTag; |
| |
| String readValueBody, writeValueBody; |
| |
| private DelegateFieldAccess delegateFieldAccess; |
| |
| private org.apache.tapestry5.services.FieldAccess access; |
| |
| TransformFieldImpl(CtField field, boolean added) |
| { |
| this.field = field; |
| this.name = field.getName(); |
| this.added = added; |
| |
| try |
| { |
| fieldType = field.getType(); |
| type = fieldType.getName(); |
| } |
| catch (NotFoundException ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| void doFinish() |
| { |
| if (delegateFieldAccess != null) |
| { |
| access = createAccess(); |
| delegateFieldAccess.delegate = access; |
| delegateFieldAccess = null; |
| } |
| } |
| |
| @Override |
| public String toString() |
| { |
| return String.format("TransformField[%s %s.%s(%s)]", Modifier.toString(field.getModifiers()), |
| getClassName(), name, type); |
| } |
| |
| public int compareTo(TransformField o) |
| { |
| return name.compareTo(o.getName()); |
| } |
| |
| public String getName() |
| { |
| return name; |
| } |
| |
| public String getType() |
| { |
| return type; |
| } |
| |
| public <T extends Annotation> T getAnnotation(Class<T> annotationClass) |
| { |
| failIfFrozen(); |
| |
| if (annotations == null) |
| annotations = extractAnnotations(field); |
| |
| return findAnnotationInList(annotationClass, annotations); |
| } |
| |
| public void claim(Object tag) |
| { |
| assert tag != null; |
| |
| failIfFrozen(); |
| |
| if (claimTag != null) |
| throw new IllegalStateException(String.format( |
| "Field %s of class %s is already claimed by %s and can not be claimed by %s.", getName(), |
| getClassName(), claimTag, tag)); |
| |
| claimTag = tag; |
| |
| formatter.format("Field %s claimed by %s\n\n", name, tag); |
| } |
| |
| public boolean isClaimed() |
| { |
| return claimTag != null; |
| } |
| |
| public int getModifiers() |
| { |
| return field.getModifiers(); |
| } |
| |
| void replaceReadAccess(String methodName) |
| { |
| failIfFrozen(); |
| |
| if (readValueBody != null) |
| throw new IllegalStateException(String.format("Field %s.%s has already had read access replaced.", |
| getClassName(), name)); |
| |
| // Explicitly reference $0 (aka "this") because of TAPESTRY-1511. |
| // $0 is valid even inside a static method. |
| |
| readValueBody = String.format("$_ = $0.%s();", methodName); |
| |
| formatter.format("replace read %s: %s();\n\n", name, methodName); |
| |
| fieldAccessReplaced = true; |
| } |
| |
| void replaceWriteAccess(String methodName) |
| { |
| failIfFrozen(); |
| |
| if (writeValueBody != null) |
| throw new IllegalStateException(String.format("Field %s.%s has already had write access replaced.", |
| getClassName(), name)); |
| |
| // Explicitly reference $0 (aka "this") because of TAPESTRY-1511. |
| // $0 is valid even inside a static method. |
| |
| writeValueBody = String.format("$0.%s($1);", methodName); |
| |
| formatter.format("replace write %s: %s();\n\n", name, methodName); |
| |
| fieldAccessReplaced = true; |
| } |
| |
| public org.apache.tapestry5.services.FieldAccess getAccess() |
| { |
| failIfFrozen(); |
| |
| if (access != null) |
| return access; |
| |
| if (delegateFieldAccess == null) |
| { |
| delegateFieldAccess = new DelegateFieldAccess(); |
| } |
| |
| return delegateFieldAccess; |
| } |
| |
| private org.apache.tapestry5.services.FieldAccess createAccess() |
| { |
| TransformMethod reader = createReader(); |
| TransformMethod writer = createWriter(); |
| |
| return createFieldAccess(reader, writer); |
| } |
| |
| private org.apache.tapestry5.services.FieldAccess createFieldAccess(TransformMethod reader, |
| TransformMethod writer) |
| { |
| ClassFab cf = classFactory.newClass(org.apache.tapestry5.services.FieldAccess.class); |
| |
| addFieldAccessReadMethod(cf, reader); |
| addFieldAccessWriteMethod(cf, writer); |
| |
| cf.addToString(String.format("FieldAccess<%s.%s>", getClassName(), name)); |
| |
| Class accessClass = cf.createClass(); |
| |
| try |
| { |
| return (org.apache.tapestry5.services.FieldAccess) accessClass.newInstance(); |
| } |
| catch (Exception ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| private void addFieldAccessReadMethod(ClassFab cf, TransformMethod readAccess) |
| { |
| BodyBuilder builder = new BodyBuilder().begin(); |
| |
| builder.addln("%s instance = (%<s) $1;", getClassName()); |
| builder.addln("return ($w) instance.%s();", readAccess.getName()); |
| |
| builder.end(); |
| |
| cf.addMethod(Modifier.PUBLIC, FIELD_ACCESS_READ_SIGNATURE, builder.toString()); |
| } |
| |
| private void addFieldAccessWriteMethod(ClassFab cf, TransformMethod writeAccess) |
| { |
| BodyBuilder builder = new BodyBuilder().begin(); |
| |
| builder.addln("%s instance = (%<s) $1;", getClassName()); |
| builder.addln("%s value = %s;", type, ClassFabUtils.castReference("$2", type)); |
| builder.addln("instance.%s(value);", writeAccess.getName()); |
| |
| builder.end(); |
| |
| cf.addMethod(Modifier.PUBLIC, FIELD_ACCESS_WRITE_SIGNATURE, builder.toString()); |
| } |
| |
| private TransformMethod createReader() |
| { |
| String methodName = newMemberName("readaccess", name); |
| |
| TransformMethodSignature signature = new TransformMethodSignature(Modifier.PUBLIC, type, methodName, null, |
| null); |
| |
| // Add the method as existing, so that the field access may be replaced with a FieldValueConduit |
| // if necessary. |
| return addOrReplaceMethod(signature, String.format("return %s;", name), false); |
| } |
| |
| private TransformMethod createWriter() |
| { |
| String methodName = newMemberName("writeaccess", name); |
| |
| TransformMethodSignature signature = new TransformMethodSignature(Modifier.PUBLIC, "void", methodName, |
| new String[] |
| { type }, null); |
| |
| // Add the method as existing, so that the field access may be replaced with a FieldValueConduit |
| // if necessary. |
| |
| return addOrReplaceMethod(signature, String.format("%s = $1;", name), false); |
| } |
| |
| public void replaceAccess(ComponentValueProvider<FieldValueConduit> conduitProvider) |
| { |
| replaceAccess(addIndirectInjectedField(FieldValueConduit.class, name + "$conduit", conduitProvider)); |
| } |
| |
| public void replaceAccess(FieldValueConduit conduit) |
| { |
| String fieldName = addInjectedFieldUncached(FieldValueConduit.class, name + "$conduit", conduit); |
| |
| // TODO: If access != null? |
| |
| access = toFieldAccess(conduit); |
| |
| if (delegateFieldAccess != null) |
| { |
| delegateFieldAccess.delegate = access; |
| delegateFieldAccess = null; |
| } |
| |
| replaceAccess(getTransformFieldImpl(fieldName)); |
| } |
| |
| public void replaceAccess(TransformField conduitField) |
| { |
| failIfFrozen(); |
| |
| String conduitFieldName = conduitField.getName(); |
| |
| String readMethodName = newMemberName("get", name); |
| |
| TransformMethodSignature readSig = new TransformMethodSignature(Modifier.PRIVATE, type, readMethodName, |
| null, null); |
| |
| String cast = TransformUtils.getWrapperTypeName(type); |
| |
| // The ($r) cast will convert the result to the method return type; generally |
| // this does nothing. but for primitive types, it will unwrap |
| // the wrapper type back to a primitive. |
| |
| BodyBuilder builder = new BodyBuilder(); |
| |
| builder.begin(); |
| |
| builder.addln("%s result = ($r) ((%s) %s.get());", type, cast, conduitFieldName); |
| |
| if (developmentMode) |
| { |
| builder.addln("%s = result;", name); |
| } |
| |
| builder.addln("return result;"); |
| |
| builder.end(); |
| |
| addNewMethod(readSig, builder.toString()); |
| |
| replaceReadAccess(readMethodName); |
| |
| String writeMethodName = newMemberName("set", name); |
| |
| TransformMethodSignature writeSig = new TransformMethodSignature(Modifier.PRIVATE, "void", writeMethodName, |
| new String[] |
| { type }, null); |
| |
| builder.clear().begin(); |
| |
| if (developmentMode) |
| { |
| builder.addln("%s = $1;", name); |
| } |
| |
| builder.addln("%s.set(($w) $1);", conduitFieldName); |
| |
| builder.end(); |
| |
| addNewMethod(writeSig, builder.toString()); |
| |
| replaceWriteAccess(writeMethodName); |
| } |
| |
| public void inject(Object value) |
| { |
| failIfFrozen(); |
| |
| addInjectToConstructor(name, fieldType, value); |
| |
| makeReadOnly(name); |
| } |
| |
| public <T> void injectIndirect(ComponentValueProvider<T> provider) |
| { |
| assert provider != null; |
| |
| failIfFrozen(); |
| |
| String argReference = addConstructorArg(providerType, provider); |
| |
| addToConstructor(String.format(" %s = (%s) (%s).get(%s);", name, type, argReference, resourcesFieldName)); |
| |
| makeReadOnly(name); |
| } |
| |
| } |
| |
| private final Map<TransformMethodSignature, TransformMethodImpl> methods = CollectionFactory.newMap(); |
| |
| private Map<String, TransformFieldImpl> fields = CollectionFactory.newMap(); |
| |
| /** |
| * Map, keyed on InjectKey, of field name. Injections are always added as protected (not |
| * private) fields to support |
| * sharing of injections between a base class and a sub class. |
| */ |
| private final Map<InjectionKey, String> injectionCache = CollectionFactory.newMap(); |
| |
| // Cache of class annotation |
| |
| private List<Annotation> classAnnotations; |
| |
| /** |
| * Contains the assembled Javassist code for the class' default constructor. |
| */ |
| private StringBuilder constructor = new StringBuilder(INIT_BUFFER_SIZE); |
| |
| private final List<Object> constructorArgs; |
| |
| private final MutableComponentModel componentModel; |
| |
| private final String resourcesFieldName; |
| |
| private 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; |
| |
| // In development mode, extra logic is inserted to "shadow" fields (that have been replaced with a FieldConduit). |
| // The "live" value is stored into the field any time it is read or updated, to assist with debugging. |
| // https://issues.apache.org/jira/browse/TAP5-1208 |
| private final boolean developmentMode; |
| |
| // If true, then during finish, it is necessary to search for field replacements |
| // (field reads or writes replaces with method calls). |
| private boolean fieldAccessReplaced; |
| |
| /** |
| * Signature for newInstance() method of Instantiator. |
| */ |
| private static final MethodSignature NEW_INSTANCE_SIGNATURE = new MethodSignature(Component.class, "newInstance", |
| new Class[] |
| { InternalComponentResources.class }, null); |
| |
| private static final TransformMethodSignature GET_COMPONENT_RESOURCES_SIGNATURE = new TransformMethodSignature( |
| Modifier.PUBLIC | Modifier.FINAL, ComponentResources.class.getName(), "getComponentResources", null, null); |
| |
| /** |
| * This is a constructor for a base class. |
| */ |
| public InternalClassTransformationImpl(ClassFactory classFactory, CtClass ctClass, |
| ComponentClassCache componentClassCache, MutableComponentModel componentModel, CtClassSource classSource, |
| boolean developmentMode) |
| { |
| this.ctClass = ctClass; |
| this.componentClassCache = componentClassCache; |
| this.classSource = classSource; |
| this.developmentMode = developmentMode; |
| classPool = this.ctClass.getClassPool(); |
| this.classFactory = classFactory; |
| parentTransformation = null; |
| this.componentModel = componentModel; |
| |
| providerType = toCtClass(ComponentValueProvider.class); |
| |
| idAllocator = new IdAllocator(); |
| |
| logger = componentModel.getLogger(); |
| |
| preloadMembers(); |
| |
| constructorArgs = CollectionFactory.newList(); |
| constructor.append("{\n"); |
| |
| addImplementedInterface(Component.class); |
| |
| resourcesFieldName = addField(Modifier.PROTECTED | Modifier.FINAL, InternalComponentResources.class.getName(), |
| "resources"); |
| |
| addToConstructor(String.format(" %s = $1;", resourcesFieldName)); |
| |
| addNewMethod(GET_COMPONENT_RESOURCES_SIGNATURE, "return " + resourcesFieldName + ";"); |
| |
| // The "}" will be added later, inside finish(). |
| } |
| |
| /** |
| * Constructor for a component sub-class. |
| * |
| * @param developmentMode |
| * TODO |
| */ |
| private InternalClassTransformationImpl(CtClass ctClass, InternalClassTransformation parentTransformation, |
| ClassFactory classFactory, CtClassSource classSource, ComponentClassCache componentClassCache, |
| MutableComponentModel componentModel, boolean developmentMode) |
| { |
| 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; |
| this.developmentMode = developmentMode; |
| |
| providerType = toCtClass(ComponentValueProvider.class); |
| |
| resourcesFieldName = parentTransformation.getResourcesFieldName(); |
| |
| idAllocator = parentTransformation.getIdAllocator(); |
| |
| preloadMembers(); |
| |
| constructorArgs = parentTransformation.getConstructorArgs(); |
| |
| // Re-invoke the constructor, passing the resources and array of values to the super class |
| |
| addToConstructor("{\n super($$);"); |
| |
| // The "}" will be added later, inside finish(). |
| } |
| |
| public InternalClassTransformation createChildTransformation(CtClass childClass, MutableComponentModel childModel) |
| { |
| return new InternalClassTransformationImpl(childClass, this, classFactory, classSource, componentClassCache, |
| childModel, developmentMode); |
| } |
| |
| private void freeze() |
| { |
| frozen = true; |
| |
| // Free up stuff we don't need after freezing. |
| // Everything else should be final. |
| |
| fields = null; |
| |
| classAnnotations = null; |
| constructor = null; |
| formatter = null; |
| description = null; |
| } |
| |
| public String getResourcesFieldName() |
| { |
| return resourcesFieldName; |
| } |
| |
| /** |
| * Loads all existing fields and methods defined by the class. |
| */ |
| private void preloadMembers() |
| { |
| preloadFields(); |
| preloadMethods(); |
| } |
| |
| private void preloadMethods() |
| { |
| for (CtMethod method : ctClass.getDeclaredMethods()) |
| { |
| recordMethod(method, false); |
| |
| idAllocator.allocateId(method.getName()); |
| } |
| } |
| |
| /** |
| * Converts and stores {@link CtField} to {@link TransformField}, and checks that each field is |
| * one of: |
| * <ul> |
| * <li>private</li> |
| * <li>static</li> |
| * <li>groovy.lang.MetaClass (for Groovy compatibility)</li> </li> |
| */ |
| private void preloadFields() |
| { |
| List<String> names = CollectionFactory.newList(); |
| |
| for (CtField field : ctClass.getDeclaredFields()) |
| { |
| String name = field.getName(); |
| |
| idAllocator.allocateId(name); |
| |
| TransformFieldImpl tfi = fields.put(name, new TransformFieldImpl(field, false)); |
| |
| 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")) |
| { |
| tfi.claim("Ignored"); |
| continue; |
| } |
| |
| names.add(name); |
| } |
| |
| if (!names.isEmpty()) |
| throw new RuntimeException(ServicesMessages.nonPrivateFields(getClassName(), names)); |
| } |
| |
| public <T extends Annotation> T getFieldAnnotation(String fieldName, Class<T> annotationClass) |
| { |
| return getField(fieldName).getAnnotation(annotationClass); |
| } |
| |
| public <T extends Annotation> T getMethodAnnotation(TransformMethodSignature signature, Class<T> annotationClass) |
| { |
| return getOrCreateMethod(signature).getAnnotation(annotationClass); |
| } |
| |
| /** |
| * 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> extractAnnotations(CtMember member) |
| { |
| try |
| { |
| List<Annotation> result = CollectionFactory.newList(); |
| |
| addAnnotationsToList(result, member.getAnnotations(), false); |
| |
| return result; |
| } |
| catch (ClassNotFoundException ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| private void addAnnotationsToList(List<Annotation> list, Object[] annotations, boolean filterNonInherited) |
| { |
| for (Object o : annotations) |
| { |
| Annotation a = (Annotation) o; |
| |
| // When assembling class annotations from a base class, you want to ignore any |
| // that are not @Inherited. |
| |
| if (filterNonInherited) |
| { |
| Class<? extends Annotation> annotationType = a.annotationType(); |
| |
| Inherited inherited = annotationType.getAnnotation(Inherited.class); |
| |
| if (inherited == null) |
| continue; |
| } |
| |
| list.add(a); |
| } |
| } |
| |
| public TransformField getField(String fieldName) |
| { |
| return getTransformFieldImpl(fieldName); |
| } |
| |
| private TransformFieldImpl getTransformFieldImpl(String fieldName) |
| { |
| failIfFrozen(); |
| |
| TransformFieldImpl result = fields.get(fieldName); |
| |
| if (result != null) |
| return result; |
| |
| throw new RuntimeException(String.format("Class %s does not contain a field named '%s'.", getClassName(), |
| fieldName)); |
| } |
| |
| public String newMemberName(String suggested) |
| { |
| failIfFrozen(); |
| |
| assert InternalUtils.isNonBlank(suggested); |
| |
| String memberName = InternalUtils.createMemberName(suggested); |
| |
| return idAllocator.allocateId(memberName); |
| } |
| |
| public String newMemberName(String prefix, String baseName) |
| { |
| return newMemberName(prefix + "_" + InternalUtils.stripMemberName(baseName)); |
| } |
| |
| public void addImplementedInterface(Class interfaceClass) |
| { |
| failIfFrozen(); |
| |
| try |
| { |
| CtClass ctInterface = toCtClass(interfaceClass); |
| |
| 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); |
| |
| TransformMethod tm = recordMethod(newMethod, true); |
| |
| addMethodToDescription("add default", tm.getSignature(), "<default>"); |
| } |
| catch (CannotCompileException ex) |
| { |
| throw new RuntimeException(ServicesMessages.errorAddingMethod(ctClass, method.getName(), ex), ex); |
| } |
| } |
| |
| private TransformMethodImpl recordMethod(CtMethod method, boolean asNew) |
| { |
| TransformMethodImpl tmi = new TransformMethodImpl(method, asNew); |
| |
| methods.put(tmi.getSignature(), tmi); |
| |
| return tmi; |
| } |
| |
| /** |
| * 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) |
| { |
| getField(fieldName).claim(tag); |
| } |
| |
| public void addMethod(TransformMethodSignature signature, String methodBody) |
| { |
| removed("addMethod(TransformMethodSignature,String)"); |
| } |
| |
| public TransformMethod addNewMethod(TransformMethodSignature signature, String methodBody) |
| { |
| return addOrReplaceMethod(signature, methodBody, true); |
| } |
| |
| public TransformMethod addNewTransformedMethod(TransformMethodSignature signature, String methodBody) |
| { |
| return addOrReplaceMethod(signature, methodBody, false); |
| } |
| |
| /** |
| * @param signature |
| * of method to add |
| * @param methodBody |
| * or null for default |
| * @param addAsNew |
| * if true, then fields in the method will not be transformed |
| */ |
| private TransformMethodImpl 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; |
| |
| TransformMethodImpl result = null; |
| |
| 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); |
| |
| result = recordMethod(method, addAsNew); |
| } |
| catch (CannotCompileException ex) |
| { |
| throw new MethodCompileException(ServicesMessages.methodCompileError(signature, methodBody, ex), |
| methodBody, ex); |
| } |
| catch (NotFoundException ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| |
| addMethodToDescription(action, signature, methodBody); |
| |
| return result; |
| } |
| |
| public void addTransformedMethod(TransformMethodSignature signature, String methodBody) |
| { |
| removed("addTransformedMethod(TransformMethodSignature,String)"); |
| } |
| |
| 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) |
| { |
| removed("extendMethod(TransformMethodSignature, String)"); |
| } |
| |
| public void extendExistingMethod(TransformMethodSignature methodSignature, String methodBody) |
| { |
| removed("extendExistingMethod(TransformMethodSignature, String)"); |
| } |
| |
| 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()); |
| |
| TransformMethodImpl tmi = locateExistingOrCreateOverrideMethod(sourceMethod); |
| |
| CtMethod source = tmi.method; |
| |
| try |
| { |
| CtMethod method = new CtMethod(returnType, newMethodName, parameters, ctClass); |
| |
| method.setModifiers(modifiers); |
| |
| method.setExceptionTypes(exceptions); |
| |
| method.setBody(source, null); |
| |
| ctClass.addMethod(method); |
| |
| recordMethod(method, false); |
| } |
| 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) |
| { |
| removed("addCatch(TransformMethodSignature, String, String)"); |
| } |
| |
| public void prefixMethod(TransformMethodSignature methodSignature, String methodBody) |
| { |
| removed("prefixMethod(TransformMethodSignature, String)"); |
| } |
| |
| 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]); |
| } |
| |
| if (methodBody != null) |
| formatter.format("\n%s", methodBody); |
| |
| description.append("\n\n"); |
| } |
| |
| public TransformMethod getOrCreateMethod(TransformMethodSignature signature) |
| { |
| failIfFrozen(); |
| |
| return findOverrideOrCreateMethod(signature); |
| } |
| |
| private TransformMethodImpl findOverrideOrCreateMethod(TransformMethodSignature signature) |
| { |
| TransformMethodImpl result = findOrOverrideMethod(signature); |
| |
| if (result != null) |
| return result; |
| |
| return addOrReplaceMethod(signature, null, true); |
| } |
| |
| private TransformMethodImpl findOrOverrideMethod(TransformMethodSignature signature) |
| { |
| TransformMethodImpl result = methods.get(signature); |
| |
| if (result != null) |
| return result; |
| |
| return addOverrideOfSuperclassMethod(signature); |
| } |
| |
| private TransformMethodImpl locateExistingOrCreateOverrideMethod(TransformMethodSignature signature) |
| { |
| TransformMethodImpl result = findOrOverrideMethod(signature); |
| |
| if (result != null) |
| return result; |
| |
| throw new IllegalArgumentException(String.format("Class %s does not declare method '%s'.", getClassName(), |
| signature)); |
| |
| } |
| |
| // TODO: Rework this method for efficiency, i.e., so that we can leverage the methods |
| // map in parent InternalClassTransformImpls, rather than the exhaustive |
| // search. |
| private TransformMethodImpl addOverrideOfSuperclassMethod(TransformMethodSignature methodSignature) |
| { |
| try |
| { |
| for (CtClass current = ctClass; current != null; current = current.getSuperclass()) |
| { |
| for (CtMethod method : current.getDeclaredMethods()) |
| { |
| if (match(method, methodSignature)) |
| { |
| // TODO: What if the method 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); |
| |
| // Record it as a new method. |
| return recordMethod(newMethod, true); |
| } |
| } |
| } |
| } |
| 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 toFieldNames(matchFieldsWithAnnotation(annotationClass)); |
| } |
| |
| public List<String> findFields(final FieldFilter filter) |
| { |
| assert filter != null; |
| |
| failIfFrozen(); |
| |
| List<TransformField> fields = matchFields(new Predicate<TransformField>() |
| { |
| public boolean accept(TransformField object) |
| { |
| return filter.accept(object.getName(), object.getType()); |
| } |
| }); |
| |
| return toFieldNames(fields); |
| } |
| |
| public List<TransformField> matchFields(Predicate<TransformField> predicate) |
| { |
| failIfFrozen(); |
| |
| return InternalUtils.matchAndSort(fields.values(), predicate); |
| } |
| |
| public List<TransformField> matchFieldsWithAnnotation(final Class<? extends Annotation> annotationClass) |
| { |
| return matchFields(new Predicate<TransformField>() |
| { |
| public boolean accept(TransformField field) |
| { |
| return field.getAnnotation(annotationClass) != null; |
| } |
| }); |
| } |
| |
| public List<TransformMethodSignature> findMethodsWithAnnotation(final Class<? extends Annotation> annotationClass) |
| { |
| List<TransformMethod> methods = matchMethods(new Predicate<TransformMethod>() |
| { |
| public boolean accept(TransformMethod method) |
| { |
| return method.getAnnotation(annotationClass) != null; |
| }; |
| }); |
| |
| return toMethodSignatures(methods); |
| } |
| |
| public List<TransformMethodSignature> findMethods(final MethodFilter filter) |
| { |
| assert filter != null; |
| |
| List<TransformMethod> methods = matchMethods(new Predicate<TransformMethod>() |
| { |
| public boolean accept(TransformMethod object) |
| { |
| return filter.accept(object.getSignature()); |
| }; |
| }); |
| |
| return toMethodSignatures(methods); |
| } |
| |
| public List<TransformMethod> matchMethods(Predicate<TransformMethod> predicate) |
| { |
| failIfFrozen(); |
| |
| return InternalUtils.matchAndSort(methods.values(), predicate); |
| } |
| |
| public List<TransformMethod> matchMethodsWithAnnotation(final Class<? extends Annotation> annotationType) |
| { |
| return matchMethods(new Predicate<TransformMethod>() |
| { |
| public boolean accept(TransformMethod method) |
| { |
| return method.getAnnotation(annotationType) != null; |
| } |
| }); |
| } |
| |
| private TransformMethodSignature toMethodSignature(CtMethod method) |
| { |
| try |
| { |
| String type = method.getReturnType().getName(); |
| String[] parameters = toTypeNames(method.getParameterTypes()); |
| String[] exceptions = toTypeNames(method.getExceptionTypes()); |
| |
| return new TransformMethodSignature(method.getModifiers(), type, method.getName(), parameters, exceptions); |
| } |
| catch (NotFoundException ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| 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() |
| { |
| return toFieldNames(matchUnclaimedFields()); |
| } |
| |
| public List<TransformField> matchUnclaimedFields() |
| { |
| return matchFields(new Predicate<TransformField>() |
| { |
| public boolean accept(TransformField object) |
| { |
| TransformFieldImpl tmi = (TransformFieldImpl) object; |
| |
| return !(tmi.added || tmi.isClaimed()); |
| } |
| }); |
| } |
| |
| public String getFieldType(String fieldName) |
| { |
| return getField(fieldName).getType(); |
| } |
| |
| public boolean isField(String fieldName) |
| { |
| failIfFrozen(); |
| |
| // Only declared instance fields end up in this map, and all |
| // fields are either static or private. |
| |
| return fields.containsKey(fieldName); |
| } |
| |
| public int getFieldModifiers(String fieldName) |
| { |
| return getField(fieldName).getModifiers(); |
| } |
| |
| public String addField(int modifiers, String type, String suggestedName) |
| { |
| return createField(modifiers, type, suggestedName).getName(); |
| } |
| |
| public TransformField createField(int modifiers, String type, String suggestedName) |
| { |
| failIfFrozen(); |
| |
| String fieldName = newMemberName(suggestedName); |
| |
| TransformFieldImpl result = null; |
| |
| try |
| { |
| CtClass ctType = convertNameToCtType(type); |
| |
| CtField field = new CtField(ctType, fieldName, ctClass); |
| field.setModifiers(modifiers); |
| |
| ctClass.addField(field); |
| |
| result = new TransformFieldImpl(field, true); |
| |
| fields.put(fieldName, result); |
| |
| } |
| 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); |
| |
| return result; |
| } |
| |
| // Returns String for backwards compatibility reasons |
| public String addInjectedField(Class type, String suggestedName, Object value) |
| { |
| assert type != null; |
| |
| failIfFrozen(); |
| |
| InjectionKey key = new InjectionKey(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; |
| } |
| |
| public <T> TransformField addIndirectInjectedField(Class<T> type, String suggestedName, |
| ComponentValueProvider<T> provider) |
| { |
| assert type != null; |
| assert provider != null; |
| |
| TransformField field = createField(Modifier.PROTECTED | Modifier.FINAL, type.getName(), suggestedName); |
| |
| String argReference = addConstructorArg(providerType, provider); |
| |
| // Inside the constructor, |
| // pass the resources to the provider's get() method, cast to the |
| // field type and assign. This will likely not work with |
| // primitives and arrays, but that's ok for now. |
| |
| addToConstructor(String.format(" %s = (%s) (%s).get(%s);", field.getName(), type.getName(), argReference, |
| resourcesFieldName)); |
| |
| return field; |
| } |
| |
| private CtClass toCtClass(Class type) |
| { |
| try |
| { |
| return classPool.get(type.getName()); |
| } |
| catch (NotFoundException ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| /** |
| * 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 = toCtClass(type); |
| |
| String fieldName = addField(Modifier.PROTECTED | Modifier.FINAL, type.getName(), suggestedName); |
| |
| addInjectToConstructor(fieldName, ctType, value); |
| |
| return fieldName; |
| } |
| |
| public String searchForPreviousInjection(InjectionKey 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) |
| { |
| getOrCreateMethod(methodSignature).addAdvice(advice); |
| } |
| |
| public boolean isMethodOverride(TransformMethodSignature methodSignature) |
| { |
| if (!isMethod(methodSignature)) |
| throw new IllegalArgumentException(String.format("Method %s is not implemented by transformed class %s.", |
| methodSignature, getClassName())); |
| |
| return getOrCreateMethod(methodSignature).isOverride(); |
| } |
| |
| public InternalClassTransformation getParentTransformation() |
| { |
| return parentTransformation; |
| } |
| |
| public boolean isMethod(TransformMethodSignature signature) |
| { |
| assert signature != null; |
| |
| return methods.containsKey(signature); |
| } |
| |
| /** |
| * 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) |
| { |
| addToConstructor(String.format(" %s = %s;", fieldName, addConstructorArg(fieldType, value))); |
| } |
| |
| public void injectField(String fieldName, Object value) |
| { |
| getField(fieldName).inject(value); |
| } |
| |
| private CtClass convertNameToCtType(String type) throws NotFoundException |
| { |
| return classPool.get(type); |
| } |
| |
| public String finish() |
| { |
| failIfFrozen(); |
| |
| // Finishing fields may sometimes create new methods, so finish the fields |
| // first. |
| |
| for (TransformFieldImpl tfi : fields.values()) |
| { |
| tfi.doFinish(); |
| } |
| |
| // doFinish() will sometimes create new methods on the ClassTransformation, yielding |
| // a concurrent modification exception, so do a defensive copy. |
| |
| List<TransformMethodImpl> tmis = CollectionFactory.newList(methods.values()); |
| |
| for (TransformMethodImpl tmi : tmis) |
| { |
| tmi.doFinish(); |
| } |
| |
| String initializer = convertConstructorToMethod(); |
| |
| performFieldTransformations(); |
| |
| addConstructor(initializer); |
| |
| String description = toString(); |
| |
| freeze(); |
| |
| return description; |
| } |
| |
| private void addConstructor(String initializer) |
| { |
| // Add a call to the initializer; the method converted from the class' default |
| // constructor. |
| |
| constructor.append(" "); |
| constructor.append(initializer); |
| |
| // This finally matches the "{" added inside the constructor |
| |
| constructor.append("();\n\n}"); |
| |
| String constructorBody = constructor.toString(); |
| |
| try |
| { |
| CtClass[] types = new CtClass[] |
| { toCtClass(InternalComponentResources.class), toCtClass(Object[].class) }; |
| |
| CtConstructor cons = CtNewConstructor.make(types, null, constructorBody, ctClass); |
| |
| ctClass.addConstructor(cons); |
| } |
| catch (CannotCompileException ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| |
| formatter.format("add constructor: %s(ComponentResources, Object[])\n%s\n\n", getClassName(), constructorBody); |
| } |
| |
| private String convertConstructorToMethod() |
| { |
| String initializer = idAllocator.allocateId("initializer"); |
| |
| try |
| { |
| CtConstructor defaultConstructor = ctClass.getConstructor("()V"); |
| |
| CtMethod initializerMethod = defaultConstructor.toMethod(initializer, ctClass); |
| |
| ctClass.addMethod(initializerMethod); |
| |
| recordMethod(initializerMethod, false); |
| |
| // Replace the constructor body with one that fails. This leaves, as an open question, |
| // what to do about any other constructors. |
| |
| String body = String.format("throw new RuntimeException(\"%s\");", |
| ServicesMessages.forbidInstantiateComponentClass(getClassName())); |
| |
| defaultConstructor.setBody(body); |
| } |
| catch (Exception ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| |
| formatter.format("convert default constructor: %s();\n\n", initializer); |
| |
| return initializer; |
| } |
| |
| public Instantiator createInstantiator() |
| { |
| if (Modifier.isAbstract(ctClass.getModifiers())) |
| return createAbstractClassInstantiator(); |
| |
| String componentClassName = getClassName(); |
| |
| String name = ClassFabUtils.generateClassName("Instantiator"); |
| |
| ClassFab cf = classFactory.newClass(name, AbstractInstantiator.class); |
| |
| Object[] componentConstructorArgs = constructorArgs.toArray(new Object[constructorArgs.size()]); |
| |
| cf.addConstructor(new Class[] |
| { ComponentModel.class, String.class, Object[].class }, null, "super($1, $2, $3);"); |
| |
| // Pass $1 (the InternalComponentResources object) and the constructorArgs (from the AbstractIntantiator |
| // base class) into the new component instance's constructor |
| |
| cf.addMethod(Modifier.PUBLIC, NEW_INSTANCE_SIGNATURE, |
| String.format("return new %s($1, constructorArgs);", componentClassName)); |
| |
| Class instantiatorClass = cf.createClass(); |
| |
| try |
| { |
| Object instance = instantiatorClass.getConstructors()[0].newInstance(componentModel, |
| String.format("Instantiator[%s]", componentClassName), componentConstructorArgs); |
| |
| return (Instantiator) instance; |
| } |
| catch (Exception ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| private Instantiator createAbstractClassInstantiator() |
| { |
| return new Instantiator() |
| { |
| public Component newInstance(InternalComponentResources resources) |
| { |
| throw new RuntimeException(String.format("Component class %s is abstract and can not be instantiated.", |
| ctClass.getName())); |
| } |
| |
| public ComponentModel getModel() |
| { |
| return componentModel; |
| } |
| }; |
| } |
| |
| private void failIfFrozen() |
| { |
| if (frozen) |
| throw new IllegalStateException("The ClassTransformation instance (for " + getClassName() |
| + ") has completed all transformations and may not be further modified."); |
| } |
| |
| private void failIfNotFrozen() |
| { |
| if (!frozen) |
| throw new IllegalStateException("The ClassTransformation instance (for " + getClassName() |
| + ") has not yet completed all transformations."); |
| } |
| |
| public IdAllocator getIdAllocator() |
| { |
| failIfNotFrozen(); |
| |
| return idAllocator; |
| } |
| |
| public List<Object> getConstructorArgs() |
| { |
| failIfNotFrozen(); |
| |
| return CollectionFactory.newList(constructorArgs); |
| } |
| |
| public List<Annotation> getClassAnnotations() |
| { |
| failIfFrozen(); |
| |
| if (classAnnotations == null) |
| assembleClassAnnotations(); |
| |
| return classAnnotations; |
| } |
| |
| private void assembleClassAnnotations() |
| { |
| classAnnotations = CollectionFactory.newList(); |
| |
| boolean filter = false; |
| |
| try |
| { |
| for (CtClass current = ctClass; current != null; current = current.getSuperclass()) |
| { |
| addAnnotationsToList(classAnnotations, current.getAnnotations(), filter); |
| |
| // Super-class annotations are filtered |
| |
| filter = true; |
| } |
| } |
| catch (NotFoundException ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| catch (ClassNotFoundException ex) |
| { |
| throw new RuntimeException(ex); |
| } |
| } |
| |
| 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()); |
| } |
| |
| if (description != null) |
| 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(getClassName(), fieldName); |
| |
| String body = String.format("throw new java.lang.RuntimeException(\"%s\");", message); |
| |
| addNewMethod(sig, body); |
| |
| replaceWriteAccess(fieldName, methodName); |
| } |
| |
| public void removeField(String fieldName) |
| { |
| removed("removeField(String)"); |
| } |
| |
| public void replaceReadAccess(String fieldName, String methodName) |
| { |
| getTransformFieldImpl(fieldName).replaceReadAccess(methodName); |
| } |
| |
| public void replaceWriteAccess(String fieldName, String methodName) |
| { |
| getTransformFieldImpl(fieldName).replaceWriteAccess(methodName); |
| } |
| |
| private void performFieldTransformations() |
| { |
| // If no field transformations have been requested, then we can save ourselves some |
| // trouble! |
| |
| if (fieldAccessReplaced) |
| replaceFieldAccess(); |
| } |
| |
| static final int SYNTHETIC = 0x00001000; |
| |
| private void replaceFieldAccess() |
| { |
| final Map<String, String> fieldReadTransforms = CollectionFactory.newMap(); |
| final Map<String, String> fieldWriteTransforms = CollectionFactory.newMap(); |
| |
| for (TransformFieldImpl tfi : fields.values()) |
| { |
| putIfNotNull(fieldReadTransforms, tfi.name, tfi.readValueBody); |
| putIfNotNull(fieldWriteTransforms, tfi.name, tfi.writeValueBody); |
| } |
| |
| ExprEditor editor = new ExprEditor() |
| { |
| private final Set<CtBehavior> addedMethods = CollectionFactory.newSet(); |
| |
| { |
| for (TransformMethodImpl tmi : methods.values()) |
| { |
| if (tmi.added) |
| addedMethods.add(tmi.method); |
| } |
| } |
| |
| public void edit(FieldAccess access) throws CannotCompileException |
| { |
| CtBehavior where = access.where(); |
| |
| if (where instanceof CtConstructor) |
| return; |
| |
| // It may be access to a public field of a data object somewhere and |
| // that's ignored TAP5-1222 |
| |
| if (!access.getClassName().equals(getClassName())) |
| 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"); |
| } |
| |
| private static <K, V> void putIfNotNull(Map<K, V> map, K key, V value) |
| { |
| if (value != null) |
| map.put(key, value); |
| } |
| |
| 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) |
| { |
| removed("extendConstructor(String)"); |
| } |
| |
| void addToConstructor(String statement) |
| { |
| constructor.append(statement); |
| constructor.append("\n"); |
| } |
| |
| public String getMethodIdentifier(TransformMethodSignature signature) |
| { |
| return getOrCreateMethod(signature).getMethodIdentifier(); |
| } |
| |
| public boolean isRootTransformation() |
| { |
| return parentTransformation == null; |
| } |
| |
| /** |
| * Adds a new constructor argument to the transformed constructor. |
| * |
| * @param parameterType |
| * type of parameter |
| * @param value |
| * value of parameter |
| * @return de-referenced argument value |
| */ |
| private String addConstructorArg(CtClass parameterType, Object value) |
| { |
| |
| int index = constructorArgs.size(); |
| |
| constructorArgs.add(value); |
| |
| return ClassFabUtils.castReference(String.format("$2[%d]", index), parameterType.getName()); |
| } |
| |
| private static List<TransformMethodSignature> toMethodSignatures(List<TransformMethod> input) |
| { |
| List<TransformMethodSignature> result = CollectionFactory.newList(); |
| |
| for (TransformMethod m : input) |
| { |
| result.add(m.getSignature()); |
| } |
| |
| return result; |
| } |
| |
| private static List<String> toFieldNames(List<TransformField> fields) |
| { |
| List<String> result = CollectionFactory.newList(); |
| |
| for (TransformField f : fields) |
| { |
| result.add(f.getName()); |
| } |
| |
| return result; |
| } |
| |
| private ComponentMethodInvocationBuilder createBuilder(TransformMethodSignature signature) |
| { |
| return new ComponentMethodInvocationBuilder(this, componentClassCache, signature, classSource); |
| } |
| |
| public boolean isDeclaredMethod(TransformMethodSignature signature) |
| { |
| failIfFrozen(); |
| |
| assert signature != null; |
| |
| return methods.containsKey(signature); |
| } |
| |
| private void removed(String methodName) |
| { |
| throw new RuntimeException(String.format( |
| "Method ClassTransformation.%s has been deprecated and is no longer functional. " |
| + "Please consult the JavaDoc for a suitable replacement.", methodName)); |
| } |
| |
| private static ComponentMethodAdvice toBeforeAdvice(final ComponentInstanceOperation operation) |
| { |
| return new ComponentMethodAdvice() |
| { |
| |
| public void advise(ComponentMethodInvocation invocation) |
| { |
| operation.invoke(invocation.getInstance()); |
| |
| invocation.proceed(); |
| } |
| }; |
| } |
| |
| private static ComponentMethodAdvice toAfterAdvice(final ComponentInstanceOperation operation) |
| { |
| return new ComponentMethodAdvice() |
| { |
| public void advise(ComponentMethodInvocation invocation) |
| { |
| invocation.proceed(); |
| |
| operation.invoke(invocation.getInstance()); |
| } |
| }; |
| } |
| |
| public void addComponentEventHandler(String eventType, int minContextValues, String methodDescription, |
| ComponentEventHandler handler) |
| { |
| assert InternalUtils.isNonBlank(eventType); |
| assert InternalUtils.isNonBlank(methodDescription); |
| assert handler != null; |
| |
| componentModel.addEventHandler(eventType); |
| |
| getOrCreateMethod(TransformConstants.DISPATCH_COMPONENT_EVENT).addAdvice( |
| createEventHandlerAdvice(eventType, minContextValues, methodDescription, handler)); |
| |
| } |
| |
| private static ComponentMethodAdvice createEventHandlerAdvice(final String eventType, final int minContextValues, |
| final String methodDescription, final ComponentEventHandler handler) |
| { |
| return new ComponentMethodAdvice() |
| { |
| public void advise(ComponentMethodInvocation invocation) |
| { |
| // Invoke the super-class implementation first. |
| |
| invocation.proceed(); |
| |
| ComponentEvent event = (ComponentEvent) invocation.getParameter(0); |
| |
| if (!event.isAborted() && event.matches(eventType, "", minContextValues)) |
| { |
| event.setMethodDescription(methodDescription); |
| |
| handler.handleEvent(invocation.getInstance(), event); |
| |
| // Ensure that the caller knows that some event handler method |
| // was invoked. |
| invocation.overrideResult(true); |
| } |
| } |
| }; |
| } |
| |
| private static org.apache.tapestry5.services.FieldAccess toFieldAccess(final FieldValueConduit conduit) |
| { |
| return new org.apache.tapestry5.services.FieldAccess() |
| { |
| |
| public void write(Object instance, Object value) |
| { |
| conduit.set(value); |
| } |
| |
| public Object read(Object instance) |
| { |
| return conduit.get(); |
| } |
| }; |
| } |
| } |