    001    // Copyright 2006, 2007, 2008, 2010, 2011, 2012 The Apache Software Foundation
    002    //
    003    // Licensed under the Apache License, Version 2.0 (the "License");
    004    // you may not use this file except in compliance with the License.
    005    // You may obtain a copy of the License at
    006    //
    007    // http://www.apache.org/licenses/LICENSE-2.0
    008    //
    009    // Unless required by applicable law or agreed to in writing, software
    010    // distributed under the License is distributed on an "AS IS" BASIS,
    011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012    // See the License for the specific language governing permissions and
    013    // limitations under the License.
    014
    015    package org.apache.tapestry5.internal.services;
    016
    017    import org.apache.tapestry5.ComponentResources;
    018    import org.apache.tapestry5.SymbolConstants;
    019    import org.apache.tapestry5.internal.InternalComponentResources;
    020    import org.apache.tapestry5.internal.InternalConstants;
    021    import org.apache.tapestry5.internal.model.MutableComponentModelImpl;
    022    import org.apache.tapestry5.internal.plastic.PlasticInternalUtils;
    023    import org.apache.tapestry5.ioc.Invokable;
    024    import org.apache.tapestry5.ioc.LoggerSource;
    025    import org.apache.tapestry5.ioc.OperationTracker;
    026    import org.apache.tapestry5.ioc.Resource;
    027    import org.apache.tapestry5.ioc.annotations.PostInjection;
    028    import org.apache.tapestry5.ioc.annotations.Primary;
    029    import org.apache.tapestry5.ioc.annotations.Symbol;
    030    import org.apache.tapestry5.ioc.internal.services.ClassFactoryImpl;
    031    import org.apache.tapestry5.ioc.internal.services.PlasticProxyFactoryImpl;
    032    import org.apache.tapestry5.ioc.internal.util.ClasspathResource;
    033    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    034    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    035    import org.apache.tapestry5.ioc.internal.util.URLChangeTracker;
    036    import org.apache.tapestry5.ioc.services.Builtin;
    037    import org.apache.tapestry5.ioc.services.ClassFactory;
    038    import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
    039    import org.apache.tapestry5.ioc.services.PlasticProxyFactory;
    040    import org.apache.tapestry5.model.ComponentModel;
    041    import org.apache.tapestry5.model.MutableComponentModel;
    042    import org.apache.tapestry5.plastic.*;
    043    import org.apache.tapestry5.plastic.PlasticManager.PlasticManagerBuilder;
    044    import org.apache.tapestry5.runtime.Component;
    045    import org.apache.tapestry5.runtime.ComponentEvent;
    046    import org.apache.tapestry5.runtime.ComponentResourcesAware;
    047    import org.apache.tapestry5.runtime.PageLifecycleListener;
    048    import org.apache.tapestry5.services.*;
    049    import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
    050    import org.apache.tapestry5.services.transform.ControlledPackageType;
    051    import org.apache.tapestry5.services.transform.TransformationSupport;
    052    import org.slf4j.Logger;
    053
    054    import java.util.Map;
    055    import java.util.Set;
    056
    057    /**
    058     * A wrapper around a {@link PlasticManager} that allows certain classes to be modified as they are loaded.
    059     */
    060    public final class ComponentInstantiatorSourceImpl implements ComponentInstantiatorSource, UpdateListener,
    061            InvalidationListener, PlasticManagerDelegate, PlasticClassListener
    062    {
    063        private final Set<String> controlledPackageNames = CollectionFactory.newSet();
    064
    065        private final URLChangeTracker changeTracker;
    066
    067        private final ClassLoader parent;
    068
    069        private final ComponentClassTransformWorker2 transformerChain;
    070
    071        private final LoggerSource loggerSource;
    072
    073        private final Logger logger;
    074
    075        private final OperationTracker tracker;
    076
    077        private final InternalComponentInvalidationEventHub invalidationHub;
    078
    079        private final boolean productionMode;
    080
    081        private final ComponentClassResolver resolver;
    082
    083        // These change whenever the invalidation event hub sends an invalidation notification
    084
    085        private volatile ClassFactory classFactory;
    086
    087        private volatile PlasticProxyFactory proxyFactory;
    088
    089        private volatile PlasticManager manager;
    090
    091        /**
    092         * Map from class name to Instantiator.
    093         */
    094        private final Map<String, Instantiator> classToInstantiator = CollectionFactory.newMap();
    095
    096        private final Map<String, ComponentModel> classToModel = CollectionFactory.newMap();
    097
    098        private final MethodDescription GET_COMPONENT_RESOURCES = PlasticUtils.getMethodDescription(
    099                ComponentResourcesAware.class, "getComponentResources");
    100
    101        private final ConstructorCallback REGISTER_AS_PAGE_LIFECYCLE_LISTENER = new ConstructorCallback()
    102        {
    103            public void onConstruct(Object instance, InstanceContext context)
    104            {
    105                InternalComponentResources resources = context.get(InternalComponentResources.class);
    106
    107                resources.addPageLifecycleListener((PageLifecycleListener) instance);
    108            }
    109        };
    110
    111        public ComponentInstantiatorSourceImpl(Logger logger,
    112
    113                                               LoggerSource loggerSource,
    114
    115                                               @Builtin
    116                                               PlasticProxyFactory proxyFactory,
    117
    118                                               @Primary
    119                                               ComponentClassTransformWorker2 transformerChain,
    120
    121                                               ClasspathURLConverter classpathURLConverter,
    122
    123                                               OperationTracker tracker,
    124
    125                                               Map<String, ControlledPackageType> configuration,
    126
    127                                               @Symbol(SymbolConstants.PRODUCTION_MODE)
    128                                               boolean productionMode,
    129
    130                                               ComponentClassResolver resolver,
    131
    132                                               InternalComponentInvalidationEventHub invalidationHub)
    133        {
    134            this.parent = proxyFactory.getClassLoader();
    135            this.transformerChain = transformerChain;
    136            this.logger = logger;
    137            this.loggerSource = loggerSource;
    138            this.changeTracker = new URLChangeTracker(classpathURLConverter);
    139            this.tracker = tracker;
    140            this.invalidationHub = invalidationHub;
    141            this.productionMode = productionMode;
    142            this.resolver = resolver;
    143
    144            // For now, we just need the keys of the configuration. When there are more types of controlled
    145            // packages, we'll need to do more.
    146
    147            controlledPackageNames.addAll(configuration.keySet());
    148
    149            initializeService();
    150        }
    151
    152        @PostInjection
    153        public void listenForUpdates(UpdateListenerHub hub)
    154        {
    155            invalidationHub.addInvalidationListener(this);
    156            hub.addUpdateListener(this);
    157        }
    158
    159        public synchronized void checkForUpdates()
    160        {
    161            if (changeTracker.containsChanges())
    162            {
    163                invalidationHub.classInControlledPackageHasChanged();
    164            }
    165        }
    166
    167        public void forceComponentInvalidation()
    168        {
    169            changeTracker.clear();
    170            invalidationHub.classInControlledPackageHasChanged();
    171        }
    172
    173        public void objectWasInvalidated()
    174        {
    175            changeTracker.clear();
    176            classToInstantiator.clear();
    177            proxyFactory.clearCache();
    178
    179            // Release the existing class pool, loader and so forth.
    180            // Create a new one.
    181
    182            initializeService();
    183        }
    184
    185        /**
    186         * Invoked at object creation, or when there are updates to class files (i.e., invalidation), to create a new set of
    187         * Javassist class pools and loaders.
    188         */
    189        private void initializeService()
    190        {
    191            PlasticManagerBuilder builder = PlasticManager.withClassLoader(parent).delegate(this)
    192                    .packages(controlledPackageNames);
    193
    194            if (!productionMode)
    195            {
    196                builder.enable(TransformationOption.FIELD_WRITEBEHIND);
    197            }
    198
    199            manager = builder.create();
    200
    201            manager.addPlasticClassListener(this);
    202
    203            classFactory = new ClassFactoryImpl(manager.getClassLoader(), logger);
    204
    205            proxyFactory = new PlasticProxyFactoryImpl(manager, logger);
    206
    207            classToInstantiator.clear();
    208            classToModel.clear();
    209        }
    210
    211        public synchronized Instantiator getInstantiator(final String className)
    212        {
    213            Instantiator result = classToInstantiator.get(className);
    214
    215            if (result == null)
    216            {
    217                result = createInstantiatorForClass(className);
    218
    219                classToInstantiator.put(className, result);
    220            }
    221
    222            return result;
    223        }
    224
    225        private Instantiator createInstantiatorForClass(final String className)
    226        {
    227            return tracker.invoke(String.format("Creating instantiator for component class %s", className),
    228                    new Invokable<Instantiator>()
    229                    {
    230                        public Instantiator invoke()
    231                        {
    232                            // Force the creation of the class (and the transformation of the class). This will first
    233                            // trigger transformations of any base classes.
    234
    235                            final ClassInstantiator<Component> plasticInstantiator = manager.getClassInstantiator(className);
    236
    237                            final ComponentModel model = classToModel.get(className);
    238
    239                            return new Instantiator()
    240                            {
    241                                public Component newInstance(InternalComponentResources resources)
    242                                {
    243                                    return plasticInstantiator.with(ComponentResources.class, resources)
    244                                            .with(InternalComponentResources.class, resources).newInstance();
    245                                }
    246
    247                                public ComponentModel getModel()
    248                                {
    249                                    return model;
    250                                }
    251
    252                                @Override
    253                                public String toString()
    254                                {
    255                                    return String.format("[Instantiator[%s]", className);
    256                                }
    257                            };
    258                        }
    259                    });
    260        }
    261
    262        public boolean exists(String className)
    263        {
    264            return parent.getResource(PlasticInternalUtils.toClassPath(className)) != null;
    265        }
    266
    267        public ClassFactory getClassFactory()
    268        {
    269            return classFactory;
    270        }
    271
    272        public PlasticProxyFactory getProxyFactory()
    273        {
    274            return proxyFactory;
    275        }
    276
    277        public void transform(final PlasticClass plasticClass)
    278        {
    279            tracker.run(String.format("Running component class transformations on %s", plasticClass.getClassName()),
    280                    new Runnable()
    281                    {
    282                        public void run()
    283                        {
    284                            String className = plasticClass.getClassName();
    285                            String parentClassName = plasticClass.getSuperClassName();
    286
    287                            // The parent model may not exist, if the super class is not in a controlled package.
    288
    289                            ComponentModel parentModel = classToModel.get(parentClassName);
    290
    291                            final boolean isRoot = parentModel == null;
    292
    293                            if (isRoot
    294                                    && !(parentClassName.equals("java.lang.Object") || parentClassName
    295                                    .equals("groovy.lang.GroovyObjectSupport")))
    296                            {
    297                                String suggestedPackageName = buildSuggestedPackageName(className);
    298
    299                                throw new RuntimeException(ServicesMessages.baseClassInWrongPackage(parentClassName,
    300                                        className, suggestedPackageName));
    301                            }
    302
    303                            // Tapestry 5.2 was more sensitive that the parent class have a public no-args constructor.
    304                            // Plastic
    305                            // doesn't care, and we don't have the tools to dig that information out.
    306
    307                            Logger logger = loggerSource.getLogger(className);
    308
    309                            Resource baseResource = new ClasspathResource(parent, PlasticInternalUtils
    310                                    .toClassPath(className));
    311
    312                            changeTracker.add(baseResource.toURL());
    313
    314                            if (isRoot)
    315                            {
    316                                implementComponentInterface(plasticClass);
    317                            }
    318
    319                            boolean isPage = resolver.isPage(className);
    320
    321                            boolean superClassImplementsPageLifecycle = plasticClass.isInterfaceImplemented(PageLifecycleListener.class);
    322
    323                            final MutableComponentModel model = new MutableComponentModelImpl(className, logger, baseResource,
    324                                    parentModel, isPage);
    325
    326                            transformerChain.transform(plasticClass, new TransformationSupportImpl(plasticClass, isRoot, model), model);
    327
    328                            if (!superClassImplementsPageLifecycle && plasticClass.isInterfaceImplemented(PageLifecycleListener.class))
    329                            {
    330                                plasticClass.onConstruct(REGISTER_AS_PAGE_LIFECYCLE_LISTENER);
    331                            }
    332
    333                            classToModel.put(className, model);
    334                        }
    335                    });
    336        }
    337
    338        private void implementComponentInterface(PlasticClass plasticClass)
    339        {
    340            plasticClass.introduceInterface(Component.class);
    341
    342            final PlasticField resourcesField = plasticClass.introduceField(InternalComponentResources.class,
    343                    "internalComponentResources").injectFromInstanceContext();
    344
    345            plasticClass.introduceMethod(GET_COMPONENT_RESOURCES, new InstructionBuilderCallback()
    346            {
    347                public void doBuild(InstructionBuilder builder)
    348                {
    349                    builder.loadThis().getField(resourcesField).returnResult();
    350                }
    351            });
    352        }
    353
    354        public <T> ClassInstantiator<T> configureInstantiator(String className, ClassInstantiator<T> instantiator)
    355        {
    356            return instantiator;
    357        }
    358
    359        private String buildSuggestedPackageName(String className)
    360        {
    361            for (String subpackage : InternalConstants.SUBPACKAGES)
    362            {
    363                String term = "." + subpackage + ".";
    364
    365                int pos = className.indexOf(term);
    366
    367                // Keep the leading '.' in the subpackage name and tack on "base".
    368
    369                if (pos > 0)
    370                    return className.substring(0, pos + 1) + InternalConstants.BASE_SUBPACKAGE;
    371            }
    372
    373            // Is this even reachable? className should always be in a controlled package and so
    374            // some subpackage above should have matched.
    375
    376            return null;
    377        }
    378
    379        public void classWillLoad(PlasticClassEvent event)
    380        {
    381            Logger logger = loggerSource.getLogger("tapestry.transformer." + event.getPrimaryClassName());
    382
    383            if (logger.isDebugEnabled())
    384                logger.debug(event.getDissasembledBytecode());
    385        }
    386
    387        private class TransformationSupportImpl implements TransformationSupport
    388        {
    389            private final PlasticClass plasticClass;
    390
    391            private final boolean root;
    392
    393            private final MutableComponentModel model;
    394
    395            public TransformationSupportImpl(PlasticClass plasticClass, boolean root, MutableComponentModel model)
    396            {
    397                this.plasticClass = plasticClass;
    398                this.root = root;
    399                this.model = model;
    400            }
    401
    402            public Class toClass(String typeName)
    403            {
    404                try
    405                {
    406                    return PlasticInternalUtils.toClass(manager.getClassLoader(), typeName);
    407                } catch (ClassNotFoundException ex)
    408                {
    409                    throw new RuntimeException(String.format(
    410                            "Unable to convert type '%s' to a Class: %s", typeName,
    411                            InternalUtils.toMessage(ex)), ex);
    412                }
    413            }
    414
    415            public boolean isRootTransformation()
    416            {
    417                return root;
    418            }
    419
    420            public void addEventHandler(final String eventType, final int minContextValues, final String operationDescription, final ComponentEventHandler handler)
    421            {
    422                assert InternalUtils.isNonBlank(eventType);
    423                assert minContextValues >= 0;
    424                assert handler != null;
    425
    426                model.addEventHandler(eventType);
    427
    428                MethodAdvice advice = new EventMethodAdvice(tracker, eventType, minContextValues, operationDescription, handler);
    429
    430                plasticClass.introduceMethod(TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION).addAdvice(advice);
    431            }
    432        }
    433
    434        private static class EventMethodAdvice implements MethodAdvice
    435        {
    436            final OperationTracker tracker;
    437            final String eventType;
    438            final int minContextValues;
    439            final String operationDescription;
    440            final ComponentEventHandler handler;
    441
    442            public EventMethodAdvice(OperationTracker tracker, String eventType, int minContextValues, String operationDescription, ComponentEventHandler handler)
    443            {
    444                this.tracker = tracker;
    445                this.eventType = eventType;
    446                this.minContextValues = minContextValues;
    447                this.operationDescription = operationDescription;
    448                this.handler = handler;
    449            }
    450
    451            public void advise(MethodInvocation invocation)
    452            {
    453                final ComponentEvent event = (ComponentEvent) invocation.getParameter(0);
    454
    455                boolean matches = !event.isAborted() && event.matches(eventType, "", minContextValues);
    456
    457                if (matches)
    458                {
    459                    final Component instance = (Component) invocation.getInstance();
    460
    461                    tracker.invoke(operationDescription, new Invokable<Object>()
    462                    {
    463                        public Object invoke()
    464                        {
    465                            handler.handleEvent(instance, event);
    466
    467                            return null;
    468                        }
    469                    });
    470                }
    471
    472                // Order of operations is key here. This logic takes precedence; base class event dispatch and event handler methods
    473                // in the class come AFTER.
    474
    475                invocation.proceed();
    476
    477                if (matches)
    478                {
    479                    invocation.setReturnValue(true);
    480                }
    481            }
    482        };
    483
    484    }