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