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