| // 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 org.apache.tapestry5.annotations.Meta; |
| import org.apache.tapestry5.annotations.OnEvent; |
| import org.apache.tapestry5.annotations.Retain; |
| import org.apache.tapestry5.annotations.SetupRender; |
| import org.apache.tapestry5.internal.InternalComponentResources; |
| import org.apache.tapestry5.internal.model.MutableComponentModelImpl; |
| import org.apache.tapestry5.internal.test.InternalBaseTestCase; |
| import org.apache.tapestry5.internal.transform.FieldRemoval; |
| import org.apache.tapestry5.internal.transform.InheritedAnnotation; |
| import org.apache.tapestry5.internal.transform.TestPackageAwareLoader; |
| import org.apache.tapestry5.internal.transform.pages.*; |
| import org.apache.tapestry5.ioc.internal.services.ClassFactoryClassPool; |
| import org.apache.tapestry5.ioc.internal.services.ClassFactoryImpl; |
| import org.apache.tapestry5.ioc.services.ClassFactory; |
| import org.apache.tapestry5.ioc.services.PropertyAccess; |
| import org.apache.tapestry5.ioc.util.BodyBuilder; |
| import org.apache.tapestry5.model.MutableComponentModel; |
| import org.apache.tapestry5.runtime.Component; |
| import org.apache.tapestry5.runtime.ComponentResourcesAware; |
| import org.apache.tapestry5.services.ClassTransformation; |
| import org.apache.tapestry5.services.MethodFilter; |
| import org.apache.tapestry5.services.TransformMethodSignature; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.testng.annotations.AfterClass; |
| import org.testng.annotations.BeforeClass; |
| import org.testng.annotations.BeforeMethod; |
| import org.testng.annotations.Test; |
| |
| import static java.lang.Thread.currentThread; |
| import java.lang.annotation.Documented; |
| import java.lang.annotation.Target; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import static java.util.Arrays.asList; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * The tests share a number of resources, and so are run sequentially. |
| */ |
| @Test(sequential = true) |
| public class InternalClassTransformationImplTest extends InternalBaseTestCase |
| { |
| private static final String STRING_CLASS_NAME = "java.lang.String"; |
| |
| private PropertyAccess access; |
| |
| private final ClassLoader contextClassLoader = currentThread().getContextClassLoader(); |
| |
| private ClassFactory classFactory; |
| |
| private Loader loader; |
| |
| private ClassFactoryClassPool classFactoryClassPool; |
| |
| @BeforeClass |
| public void setup_access() |
| { |
| access = getService("PropertyAccess", PropertyAccess.class); |
| } |
| |
| @AfterClass |
| public void cleanup_access() |
| { |
| access = null; |
| } |
| |
| /** |
| * We need a new ClassPool for each individual test, since many of the tests will end up modifying one or more |
| * CtClass instances. |
| */ |
| @BeforeMethod |
| public void setup_classpool() |
| { |
| // _classPool = new ClassPool(); |
| |
| classFactoryClassPool = new ClassFactoryClassPool(contextClassLoader); |
| |
| loader = new TestPackageAwareLoader(contextClassLoader, classFactoryClassPool); |
| |
| // Inside Maven Surefire, the system classpath is not sufficient to find all |
| // the necessary files. |
| classFactoryClassPool.appendClassPath(new LoaderClassPath(loader)); |
| |
| Logger logger = LoggerFactory.getLogger(InternalClassTransformationImplTest.class); |
| |
| classFactory = new ClassFactoryImpl(loader, classFactoryClassPool, logger); |
| } |
| |
| private CtClass findCtClass(Class targetClass) throws NotFoundException |
| { |
| return classFactoryClassPool.get(targetClass.getName()); |
| } |
| |
| private Class toClass(CtClass ctClass) throws Exception |
| { |
| return classFactoryClassPool.toClass(ctClass, loader, null); |
| } |
| |
| @Test |
| public void new_member_name() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| |
| assertEquals(ct.newMemberName("fred"), "_$fred"); |
| assertEquals(ct.newMemberName("fred"), "_$fred_0"); |
| |
| // Here we're exposing a bit of the internal algorithm, which strips |
| // off '$' and '_' before tacking "_$" in front. |
| |
| assertEquals(ct.newMemberName("_fred"), "_$fred_1"); |
| assertEquals(ct.newMemberName("_$fred"), "_$fred_2"); |
| assertEquals(ct.newMemberName("__$___$____$_fred"), "_$fred_3"); |
| |
| // Here we're trying to force conflicts with existing declared |
| // fields and methods of the class. |
| |
| assertEquals(ct.newMemberName("_parentField"), "_$parentField"); |
| assertEquals(ct.newMemberName("conflictField"), "_$conflictField_0"); |
| assertEquals(ct.newMemberName("conflictMethod"), "_$conflictMethod_0"); |
| |
| verify(); |
| } |
| |
| @Test |
| public void new_member_name_with_prefix() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| |
| assertEquals(ct.newMemberName("prefix", "fred"), "_$prefix_fred"); |
| assertEquals(ct.newMemberName("prefix", "fred"), "_$prefix_fred_0"); |
| |
| // Here we're exposing a bit of the internal algorithm, which strips |
| // off '$' and '_' before tacking "_$" in front. |
| |
| assertEquals(ct.newMemberName("prefix", "_fred"), "_$prefix_fred_1"); |
| assertEquals(ct.newMemberName("prefix", "_$fred"), "_$prefix_fred_2"); |
| assertEquals(ct.newMemberName("prefix", "__$___$____$_fred"), "_$prefix_fred_3"); |
| |
| verify(); |
| } |
| |
| private InternalClassTransformation createClassTransformation(Class targetClass, Logger logger) |
| throws NotFoundException |
| { |
| CtClass ctClass = findCtClass(targetClass); |
| |
| MutableComponentModel model = new MutableComponentModelImpl("unknown-class", logger, null, null); |
| |
| return new InternalClassTransformationImpl(classFactory, ctClass, null, model, null); |
| } |
| |
| @Test |
| public void find_annotation_on_unknown_field() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| |
| try |
| { |
| ct.getFieldAnnotation("unknownField", Retain.class); |
| unreachable(); |
| } |
| catch (RuntimeException ex) |
| { |
| assertEquals(ex.getMessage(), |
| "Class org.apache.tapestry5.internal.transform.pages.ParentClass does not contain a field named 'unknownField'."); |
| } |
| |
| verify(); |
| } |
| |
| @Test |
| public void find_field_annotation() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| |
| Retain retain = ct.getFieldAnnotation("_annotatedField", Retain.class); |
| |
| assertNotNull(retain); |
| |
| verify(); |
| } |
| |
| @Test |
| public void field_does_not_contain_requested_annotation() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| |
| // Field with annotation, but not that annotation |
| assertNull(ct.getFieldAnnotation("_annotatedField", Override.class)); |
| |
| // Field with no annotation |
| assertNull(ct.getFieldAnnotation("_parentField", Override.class)); |
| |
| verify(); |
| } |
| |
| @Test |
| public void find_fields_with_annotation() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| |
| List<String> fields = ct.findFieldsWithAnnotation(Retain.class); |
| |
| assertEquals(fields.size(), 1); |
| assertEquals(fields.get(0), "_annotatedField"); |
| |
| verify(); |
| } |
| |
| @Test |
| public void get_field_modifiers() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(CheckFieldType.class, logger); |
| |
| assertEquals(ct.getFieldModifiers("_privateField"), Modifier.PRIVATE); |
| assertEquals(ct.getFieldModifiers("_map"), Modifier.PRIVATE + Modifier.FINAL); |
| } |
| |
| @Test |
| public void get_field_exists() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(CheckFieldType.class, logger); |
| |
| assertTrue(ct.isField("_privateField")); |
| assertFalse(ct.isField("_doesNotExist")); |
| |
| verify(); |
| } |
| |
| @Test |
| public void find_fields_with_annotation_excludes_claimed_files() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| |
| ct.claimField("_annotatedField", this); |
| |
| List<String> fields = ct.findFieldsWithAnnotation(Retain.class); |
| |
| assertTrue(fields.isEmpty()); |
| |
| verify(); |
| } |
| |
| @Test |
| public void no_fields_contain_requested_annotation() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| |
| List<String> fields = ct.findFieldsWithAnnotation(Documented.class); |
| |
| assertTrue(fields.isEmpty()); |
| |
| verify(); |
| } |
| |
| @Test |
| public void claim_fields() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ClaimedFields.class, logger); |
| |
| List<String> unclaimed = ct.findUnclaimedFields(); |
| |
| assertEquals(unclaimed, asList("_field1", "_field4", "_zzfield")); |
| |
| ct.claimField("_field4", "Fred"); |
| |
| unclaimed = ct.findUnclaimedFields(); |
| |
| assertEquals(unclaimed, asList("_field1", "_zzfield")); |
| |
| try |
| { |
| ct.claimField("_field4", "Barney"); |
| unreachable(); |
| } |
| catch (RuntimeException ex) |
| { |
| assertEquals(ex.getMessage(), |
| "Field _field4 of class org.apache.tapestry5.internal.transform.pages.ClaimedFields is already claimed by Fred and can not be claimed by Barney."); |
| } |
| |
| verify(); |
| } |
| |
| @Test |
| public void added_fields_are_not_listed_as_unclaimed_fields() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ClaimedFields.class, logger); |
| |
| ct.addField(Modifier.PRIVATE, "int", "newField"); |
| |
| List<String> unclaimed = ct.findUnclaimedFields(); |
| |
| assertEquals(unclaimed, asList("_field1", "_field4", "_zzfield")); |
| |
| verify(); |
| } |
| |
| @Test |
| public void find_class_annotations() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| |
| Meta meta = ct.getAnnotation(Meta.class); |
| |
| assertNotNull(meta); |
| |
| // Try again (the annotation will be cached). Use an annotation |
| // that will not be present. |
| |
| Target t = ct.getAnnotation(Target.class); |
| |
| assertNull(t); |
| |
| verify(); |
| } |
| |
| /** |
| * More a test of how Javassist works. Javassist does not honor the Inherited annotation for classes (this kind of |
| * makes sense, since it won't necessarily have the super-class in memory). |
| */ |
| @Test |
| public void ensure_subclasses_inherit_parent_class_annotations() throws Exception |
| { |
| // The Java runtime does honor @Inherited |
| assertNotNull(ChildClassInheritsAnnotation.class.getAnnotation(InheritedAnnotation.class)); |
| |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ChildClassInheritsAnnotation.class, logger); |
| |
| InheritedAnnotation ia = ct.getAnnotation(InheritedAnnotation.class); |
| |
| // Javassist does not, but ClassTransformation patches around that. |
| |
| assertNotNull(ia); |
| |
| verify(); |
| } |
| |
| /** |
| * These tests are really to assert my understanding of Javassist's API. I guess we should keep them around to make |
| * sure that future versions of Javassist work the same as our expectations. |
| */ |
| @Test |
| public void ensure_javassist_still_does_not_show_inherited_interfaces() throws Exception |
| { |
| CtClass ctClass = findCtClass(BarImpl.class); |
| |
| CtClass[] interfaces = ctClass.getInterfaces(); |
| |
| // Just the interfaces implemented by this particular class, not |
| // inherited interfaces. |
| |
| assertEquals(interfaces.length, 1); |
| |
| assertEquals(interfaces[0].getName(), BarInterface.class.getName()); |
| |
| CtClass parentClass = ctClass.getSuperclass(); |
| |
| interfaces = parentClass.getInterfaces(); |
| |
| assertEquals(interfaces.length, 1); |
| |
| assertEquals(interfaces[0].getName(), FooInterface.class.getName()); |
| } |
| |
| @Test |
| public void ensure_javassist_does_not_show_interface_methods_on_abstract_class() throws Exception |
| { |
| CtClass ctClass = findCtClass(AbstractFoo.class); |
| |
| CtClass[] interfaces = ctClass.getInterfaces(); |
| |
| assertEquals(interfaces.length, 1); |
| |
| assertEquals(interfaces[0].getName(), FooInterface.class.getName()); |
| |
| // In some cases, Java reflection on an abstract class implementing an interface |
| // will show the interface methods as abstract methods on the class. This seems |
| // to vary from JVM to JVM. I believe Javassist is more consistent here. |
| |
| CtMethod[] methods = ctClass.getDeclaredMethods(); |
| |
| assertEquals(methods.length, 0); |
| } |
| |
| @Test |
| public void ensure_javassist_does_not_show_extended_interface_methods_on_interface() throws Exception |
| { |
| CtClass ctClass = findCtClass(FooBarInterface.class); |
| |
| // Just want to check that an interface that extends other interfaces |
| // doesn't show those other interface's methods. |
| |
| CtMethod[] methods = ctClass.getDeclaredMethods(); |
| |
| assertEquals(methods.length, 0); |
| } |
| |
| @Test |
| public void add_injected_field() throws Exception |
| { |
| InternalComponentResources resources = mockInternalComponentResources(); |
| |
| CtClass targetObjectCtClass = findCtClass(TargetObject.class); |
| |
| Logger logger = mockLogger(); |
| MutableComponentModel model = mockMutableComponentModel(logger); |
| |
| replay(); |
| |
| InternalClassTransformation ct = new InternalClassTransformationImpl(classFactory, targetObjectCtClass, null, |
| model, null); |
| |
| // Default behavior is to add an injected field for the InternalComponentResources object, |
| // so we'll just check that. |
| |
| ct.finish(); |
| |
| Instantiator instantiator = ct.createInstantiator(); |
| |
| ComponentResourcesAware instance = instantiator.newInstance(resources); |
| |
| assertSame(instance.getComponentResources(), resources); |
| |
| verify(); |
| } |
| |
| @Test |
| public void add_injected_field_from_parent_transformation() throws Exception |
| { |
| final String value = "from the parent"; |
| |
| InternalComponentResources resources = mockInternalComponentResources(); |
| |
| CtClass targetObjectCtClass = findCtClass(TargetObject.class); |
| |
| Logger logger = mockLogger(); |
| MutableComponentModel model = mockMutableComponentModel(); |
| |
| train_getLogger(model, logger); |
| |
| replay(); |
| |
| InternalClassTransformation ct = new InternalClassTransformationImpl(classFactory, targetObjectCtClass, null, |
| model, null); |
| |
| String parentFieldName = ct.addInjectedField(String.class, "_value", value); |
| |
| // Default behavior is to add an injected field for the InternalComponentResources object, |
| // so we'll just check that. |
| |
| ct.finish(); |
| |
| // Now lets work on the subclass |
| |
| CtClass subclassCtClass = findCtClass(TargetObjectSubclass.class); |
| |
| ct = ct.createChildTransformation(subclassCtClass, model); |
| |
| String subclassFieldName = ct.addInjectedField(String.class, "_childValue", value); |
| |
| // This is what proves it is cached. |
| |
| assertEquals(subclassFieldName, parentFieldName); |
| |
| // This proves the the field is protected and can be used in subclasses. |
| |
| ct.addMethod(new TransformMethodSignature(Modifier.PUBLIC, "java.lang.String", "getValue", null, null), |
| "return " + subclassFieldName + ";"); |
| |
| ct.finish(); |
| |
| Instantiator instantiator = ct.createInstantiator(); |
| |
| Object instance = instantiator.newInstance(resources); |
| |
| Object actual = access.get(instance, "value"); |
| |
| assertSame(actual, value); |
| |
| verify(); |
| } |
| |
| @Test |
| public void add_interface_to_class() throws Exception |
| { |
| InternalComponentResources resources = mockInternalComponentResources(); |
| |
| CtClass targetObjectCtClass = findCtClass(TargetObject.class); |
| |
| Logger logger = mockLogger(); |
| MutableComponentModel model = mockMutableComponentModel(logger); |
| |
| replay(); |
| |
| InternalClassTransformation ct = new InternalClassTransformationImpl(classFactory, targetObjectCtClass, null, |
| model, null); |
| |
| ct.addImplementedInterface(FooInterface.class); |
| ct.addImplementedInterface(GetterMethodsInterface.class); |
| |
| ct.finish(); |
| |
| Class transformed = toClass(targetObjectCtClass); |
| |
| Class[] interfaces = transformed.getInterfaces(); |
| |
| assertEquals(interfaces, new Class[] { Component.class, FooInterface.class, GetterMethodsInterface.class }); |
| |
| Object target = ct.createInstantiator().newInstance(resources); |
| |
| FooInterface asFoo = (FooInterface) target; |
| |
| asFoo.foo(); |
| |
| GetterMethodsInterface getters = (GetterMethodsInterface) target; |
| |
| assertEquals(getters.getBoolean(), false); |
| assertEquals(getters.getByte(), (byte) 0); |
| assertEquals(getters.getShort(), (short) 0); |
| assertEquals(getters.getInt(), 0); |
| assertEquals(getters.getLong(), 0l); |
| assertEquals(getters.getFloat(), 0.0f); |
| assertEquals(getters.getDouble(), 0.0d); |
| assertNull(getters.getString()); |
| assertNull(getters.getObjectArray()); |
| assertNull(getters.getIntArray()); |
| |
| verify(); |
| } |
| |
| @Test |
| public void make_field_read_only() throws Exception |
| { |
| InternalComponentResources resources = mockInternalComponentResources(); |
| |
| Logger logger = mockLogger(); |
| MutableComponentModel model = mockMutableComponentModel(logger); |
| |
| replay(); |
| |
| CtClass targetObjectCtClass = findCtClass(ReadOnlyBean.class); |
| |
| InternalClassTransformation ct = new InternalClassTransformationImpl(classFactory, targetObjectCtClass, null, |
| model, null); |
| |
| ct.makeReadOnly("_value"); |
| |
| ct.finish(); |
| |
| Object target = instantiate(ReadOnlyBean.class, ct, resources); |
| |
| try |
| { |
| access.set(target, "value", "anything"); |
| unreachable(); |
| } |
| catch (RuntimeException ex) |
| { |
| // The PropertyAccess layer adds a wrapper exception around the real one. |
| |
| assertEquals(ex.getCause().getMessage(), |
| "Field org.apache.tapestry5.internal.transform.pages.ReadOnlyBean._value is read-only."); |
| } |
| |
| verify(); |
| } |
| |
| @Test |
| public void removed_fields_should_not_show_up_as_unclaimed() throws Exception |
| { |
| Logger logger = mockLogger(); |
| MutableComponentModel model = mockMutableComponentModel(logger); |
| |
| replay(); |
| |
| CtClass targetObjectCtClass = findCtClass(RemoveFieldBean.class); |
| |
| InternalClassTransformation ct = new InternalClassTransformationImpl(null, targetObjectCtClass, null, model, |
| null); |
| |
| ct.removeField("_barney"); |
| |
| assertEquals(ct.findUnclaimedFields(), asList("_fred")); |
| |
| verify(); |
| } |
| |
| @Test |
| public void add_to_constructor() throws Exception |
| { |
| InternalComponentResources resources = mockInternalComponentResources(); |
| |
| Logger logger = mockLogger(); |
| MutableComponentModel model = mockMutableComponentModel(logger); |
| |
| replay(); |
| |
| CtClass targetObjectCtClass = findCtClass(ReadOnlyBean.class); |
| |
| InternalClassTransformation ct = new InternalClassTransformationImpl(classFactory, targetObjectCtClass, null, |
| model, null); |
| |
| ct.extendConstructor("_value = \"from constructor\";"); |
| |
| ct.finish(); |
| |
| Object target = instantiate(ReadOnlyBean.class, ct, resources); |
| |
| assertEquals(access.get(target, "value"), "from constructor"); |
| |
| verify(); |
| } |
| |
| @Test |
| public void inject_field() throws Exception |
| { |
| InternalComponentResources resources = mockInternalComponentResources(); |
| |
| Logger logger = mockLogger(); |
| MutableComponentModel model = mockMutableComponentModel(logger); |
| |
| replay(); |
| |
| CtClass targetObjectCtClass = findCtClass(ReadOnlyBean.class); |
| |
| InternalClassTransformation ct = new InternalClassTransformationImpl(classFactory, targetObjectCtClass, null, |
| model, null); |
| |
| ct.injectField("_value", "Tapestry"); |
| |
| ct.finish(); |
| |
| Object target = instantiate(ReadOnlyBean.class, ct, resources); |
| |
| assertEquals(access.get(target, "value"), "Tapestry"); |
| |
| try |
| { |
| access.set(target, "value", "anything"); |
| unreachable(); |
| } |
| catch (RuntimeException ex) |
| { |
| // The PropertyAccess layer adds a wrapper exception around the real one. |
| |
| assertEquals(ex.getCause().getMessage(), |
| "Field org.apache.tapestry5.internal.transform.pages.ReadOnlyBean._value is read-only."); |
| } |
| |
| verify(); |
| } |
| |
| /** |
| * Tests the basic functionality of overriding read and write; also tests the case for multiple field read/field |
| * write substitions. |
| */ |
| @Test |
| public void override_field_read_and_write() throws Exception |
| { |
| InternalComponentResources resources = mockInternalComponentResources(); |
| |
| Logger logger = mockLogger(); |
| MutableComponentModel model = mockMutableComponentModel(logger); |
| |
| replay(); |
| |
| CtClass targetObjectCtClass = findCtClass(FieldAccessBean.class); |
| |
| InternalClassTransformation ct = new InternalClassTransformationImpl(classFactory, targetObjectCtClass, null, |
| model, null); |
| |
| replaceAccessToField(ct, "foo"); |
| replaceAccessToField(ct, "bar"); |
| |
| // Stuff ... |
| |
| ct.finish(); |
| |
| Object target = instantiate(FieldAccessBean.class, ct, resources); |
| |
| // target is no longer assignable to FieldAccessBean; its a new class from a new class |
| // loader. So we use reflective access, which doesn't care about such things. |
| |
| checkReplacedFieldAccess(target, "foo"); |
| checkReplacedFieldAccess(target, "bar"); |
| |
| verify(); |
| } |
| |
| private void checkReplacedFieldAccess(Object target, String propertyName) |
| { |
| |
| try |
| { |
| access.get(target, propertyName); |
| unreachable(); |
| } |
| catch (RuntimeException ex) |
| { |
| // PropertyAccess adds a wrapper exception |
| assertEquals(ex.getCause().getMessage(), "read " + propertyName); |
| } |
| |
| try |
| { |
| access.set(target, propertyName, "new value"); |
| unreachable(); |
| } |
| catch (RuntimeException ex) |
| { |
| // PropertyAccess adds a wrapper exception |
| assertEquals(ex.getCause().getMessage(), "write " + propertyName); |
| } |
| } |
| |
| private void replaceAccessToField(InternalClassTransformation ct, String baseName) |
| { |
| String fieldName = "_" + baseName; |
| String readMethodName = "_read_" + baseName; |
| |
| TransformMethodSignature readMethodSignature = new TransformMethodSignature(Modifier.PRIVATE, STRING_CLASS_NAME, |
| readMethodName, null, null); |
| |
| ct.addMethod(readMethodSignature, String.format("throw new RuntimeException(\"read %s\");", baseName)); |
| |
| ct.replaceReadAccess(fieldName, readMethodName); |
| |
| String writeMethodName = "_write_" + baseName; |
| |
| TransformMethodSignature writeMethodSignature = new TransformMethodSignature(Modifier.PRIVATE, "void", |
| writeMethodName, |
| new String[] { STRING_CLASS_NAME }, |
| null); |
| ct.addMethod(writeMethodSignature, String.format("throw new RuntimeException(\"write %s\");", baseName)); |
| |
| ct.replaceWriteAccess(fieldName, writeMethodName); |
| } |
| |
| @Test |
| public void find_methods_with_annotation() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(AnnotatedPage.class, logger); |
| |
| List<TransformMethodSignature> l = ct.findMethodsWithAnnotation(SetupRender.class); |
| |
| // Check order |
| |
| assertEquals(l.size(), 2); |
| assertEquals(l.get(0).toString(), "void beforeRender()"); |
| assertEquals(l.get(1).toString(), "boolean earlyRender(org.apache.tapestry5.MarkupWriter)"); |
| |
| // Check up on cacheing |
| |
| assertEquals(ct.findMethodsWithAnnotation(SetupRender.class), l); |
| |
| // Check up on no match. |
| |
| assertTrue(ct.findFieldsWithAnnotation(Deprecated.class).isEmpty()); |
| |
| verify(); |
| } |
| |
| @Test |
| public void find_methods_using_filter() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| final ClassTransformation ct = createClassTransformation(AnnotatedPage.class, logger); |
| |
| // Duplicates, somewhat less efficiently, the logic in find_methods_with_annotation(). |
| |
| MethodFilter filter = new MethodFilter() |
| { |
| public boolean accept(TransformMethodSignature signature) |
| { |
| return ct.getMethodAnnotation(signature, SetupRender.class) != null; |
| } |
| }; |
| |
| List<TransformMethodSignature> l = ct.findMethods(filter); |
| |
| // Check order |
| |
| assertEquals(l.size(), 2); |
| assertEquals(l.get(0).toString(), "void beforeRender()"); |
| assertEquals(l.get(1).toString(), "boolean earlyRender(org.apache.tapestry5.MarkupWriter)"); |
| |
| // Check up on cacheing |
| |
| assertEquals(ct.findMethodsWithAnnotation(SetupRender.class), l); |
| |
| // Check up on no match. |
| |
| assertTrue(ct.findFieldsWithAnnotation(Deprecated.class).isEmpty()); |
| |
| verify(); |
| } |
| |
| @Test |
| public void to_class_with_primitive_type() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(AnnotatedPage.class, logger); |
| |
| assertSame(ct.toClass("float"), Float.class); |
| |
| verify(); |
| } |
| |
| @Test |
| public void to_class_with_object_type() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(AnnotatedPage.class, logger); |
| |
| assertSame(ct.toClass("java.util.Map"), Map.class); |
| |
| verify(); |
| } |
| |
| @Test |
| public void non_private_fields_are_an_exception() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| |
| replay(); |
| |
| InternalClassTransformation ct = createClassTransformation(VisibilityBean.class, logger); |
| |
| try |
| { |
| |
| ct.finish(); |
| |
| unreachable(); |
| } |
| catch (RuntimeException ex) |
| { |
| assertMessageContains(ex, "Class " + VisibilityBean.class.getName() + " contains field(s)", |
| "_$myPackagePrivate", "_$myProtected", "_$myPublic"); |
| } |
| |
| verify(); |
| } |
| |
| @Test |
| public void find_annotation_in_method() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(EventHandlerTarget.class, logger); |
| |
| OnEvent annotation = ct.getMethodAnnotation(new TransformMethodSignature("handler"), OnEvent.class); |
| |
| // Check that the attributes of the annotation match the expectation. |
| |
| assertEquals(annotation.value(), "fred"); |
| assertEquals(annotation.component(), "alpha"); |
| |
| verify(); |
| } |
| |
| @Test |
| public void find_annotation_in_unknown_method() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| |
| try |
| { |
| ct.getMethodAnnotation(new TransformMethodSignature("foo"), OnEvent.class); |
| unreachable(); |
| } |
| catch (IllegalArgumentException ex) |
| { |
| assertEquals(ex.getMessage(), |
| "Class org.apache.tapestry5.internal.transform.pages.ParentClass does not declare method 'public void foo()'."); |
| } |
| |
| verify(); |
| } |
| |
| @Test |
| public void prefix_method() throws Exception |
| { |
| Logger logger = mockLogger(); |
| TransformMethodSignature sig = new TransformMethodSignature(Modifier.PUBLIC, "int", "getParentField", null, |
| null); |
| |
| replay(); |
| |
| InternalClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| ct.prefixMethod(sig, "return 42;"); |
| |
| String desc = ct.toString(); |
| assertTrue(desc.contains("prefix")); |
| assertTrue(desc.contains("getParentField")); |
| |
| // fail if frozen |
| ct.finish(); |
| try |
| { |
| ct.prefixMethod(sig, "return 0;"); |
| unreachable(); |
| } |
| catch (IllegalStateException e) |
| { |
| } |
| |
| |
| verify(); |
| } |
| |
| @Test |
| public void fields_in_prefixed_methods_are_transformed() throws Exception |
| { |
| Logger logger = mockLogger(); |
| TransformMethodSignature sig = new TransformMethodSignature(Modifier.PUBLIC, "int", "getTargetValue", null, |
| null); |
| Runnable runnable = mockRunnable(); |
| |
| runnable.run(); |
| |
| replay(); |
| |
| InternalClassTransformation ct = createClassTransformation(MethodPrefixTarget.class, logger); |
| |
| String name = ct.addInjectedField(Runnable.class, "runnable", runnable); |
| |
| // Transform the field. |
| |
| TransformMethodSignature reader = new TransformMethodSignature(Modifier.PRIVATE, "int", "read_target_value", |
| null, null); |
| |
| ct.addMethod(reader, "return 66;"); |
| |
| ct.replaceReadAccess("_targetField", "read_target_value"); |
| |
| ct.prefixMethod(sig, name + ".run();"); |
| |
| ct.finish(); |
| |
| Object target = instantiate(MethodPrefixTarget.class, ct, null); |
| |
| // 66 reflects the change to the field. |
| |
| assertEquals(access.get(target, "targetValue"), 66); |
| |
| verify(); |
| } |
| |
| private Component instantiate(Class<?> expectedClass, InternalClassTransformation ct, |
| InternalComponentResources resources) throws Exception |
| { |
| Instantiator ins = ct.createInstantiator(); |
| |
| return ins.newInstance(resources); |
| } |
| |
| @Test |
| public void extend_existing_method_fields_are_transformed() throws Exception |
| { |
| Logger logger = mockLogger(); |
| TransformMethodSignature sig = new TransformMethodSignature(Modifier.PUBLIC, "int", "getTargetValue", null, |
| null); |
| Runnable runnable = mockRunnable(); |
| |
| runnable.run(); |
| |
| replay(); |
| |
| InternalClassTransformation ct = createClassTransformation(MethodPrefixTarget.class, logger); |
| |
| String name = ct.addInjectedField(Runnable.class, "runnable", runnable); |
| |
| // Transform the field. |
| |
| TransformMethodSignature reader = new TransformMethodSignature(Modifier.PRIVATE, "int", "read_target_value", |
| null, null); |
| |
| ct.addMethod(reader, "return 66;"); |
| |
| ct.replaceReadAccess("_targetField", "read_target_value"); |
| |
| BodyBuilder builder = new BodyBuilder(); |
| builder.begin(); |
| builder.addln("%s.run();", name); |
| builder.addln("return $_ + 1;"); |
| builder.end(); |
| |
| ct.extendExistingMethod(sig, builder.toString()); |
| |
| ct.finish(); |
| |
| Object target = instantiate(MethodPrefixTarget.class, ct, null); |
| |
| // 66 reflects the change to the field, +1 reflects the extension of the method. |
| |
| assertEquals(access.get(target, "targetValue"), 67); |
| |
| verify(); |
| } |
| |
| @Test |
| public void invalid_code() throws Exception |
| { |
| Logger logger = mockLogger(); |
| TransformMethodSignature sig = new TransformMethodSignature(Modifier.PUBLIC, "int", "getParentField", null, |
| null); |
| |
| replay(); |
| |
| InternalClassTransformation ct = createClassTransformation(ParentClass.class, logger); |
| |
| try |
| { |
| ct.prefixMethod(sig, "return supercalafragalistic;"); |
| unreachable(); |
| } |
| catch (MethodCompileException ex) |
| { |
| |
| } |
| |
| verify(); |
| } |
| |
| |
| @Test |
| public void remove_field() throws Exception |
| { |
| Logger logger = mockLogger(); |
| MutableComponentModel model = mockMutableComponentModel(logger); |
| |
| replay(); |
| |
| CtClass targetObjectCtClass = findCtClass(FieldRemoval.class); |
| |
| InternalClassTransformation ct = new InternalClassTransformationImpl(classFactory, targetObjectCtClass, null, |
| model, null); |
| |
| ct.removeField("_fieldToRemove"); |
| |
| ct.finish(); |
| |
| Class transformed = toClass(targetObjectCtClass); |
| |
| for (Field f : transformed.getDeclaredFields()) |
| { |
| if (f.getName().equals("_fieldToRemove")) |
| throw new AssertionError("_fieldToRemove still in transformed class."); |
| } |
| |
| verify(); |
| } |
| |
| @Test |
| public void get_method_identifier() throws Exception |
| { |
| Logger logger = mockLogger(); |
| |
| replay(); |
| |
| ClassTransformation ct = createClassTransformation(MethodIdentifier.class, logger); |
| |
| List<TransformMethodSignature> sigs = ct.findMethodsWithAnnotation(OnEvent.class); |
| |
| assertEquals(sigs.size(), 1); |
| |
| TransformMethodSignature sig = sigs.get(0); |
| |
| assertEquals(ct.getMethodIdentifier(sig), |
| "org.apache.tapestry5.internal.transform.pages.MethodIdentifier.makeWaves(java.lang.String, int[]) (at MethodIdentifier.java:24)"); |
| |
| verify(); |
| } |
| |
| |
| } |