    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.newMap();
    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 synchronized Instantiator getInstantiator(final String className)
    204    {
    205        Instantiator result = classToInstantiator.get(className);
    206
    207        if (result == null)
    208        {
    209            result = createInstantiatorForClass(className);
    210
    211            classToInstantiator.put(className, result);
    212        }
    213
    214        return result;
    215    }
    216
    217    private Instantiator createInstantiatorForClass(final String className)
    218    {
    219        return tracker.invoke(String.format("Creating instantiator for component class %s", className),
    220                new Invokable<Instantiator>()
    221                {
    222                    public Instantiator invoke()
    223                    {
    224                        // Force the creation of the class (and the transformation of the class). This will first
    225                        // trigger transformations of any base classes.
    226
    227                        final ClassInstantiator<Component> plasticInstantiator = manager.getClassInstantiator(className);
    228
    229                        final ComponentModel model = classToModel.get(className);
    230
    231                        return new Instantiator()
    232                        {
    233                            public Component newInstance(InternalComponentResources resources)
    234                            {
    235                                return plasticInstantiator.with(ComponentResources.class, resources)
    236                                        .with(InternalComponentResources.class, resources).newInstance();
    237                            }
    238
    239                            public ComponentModel getModel()
    240                            {
    241                                return model;
    242                            }
    243
    244                            @Override
    245                            public String toString()
    246                            {
    247                                return String.format("[Instantiator[%s]", className);
    248                            }
    249                        };
    250                    }
    251                });
    252    }
    253
    254    public boolean exists(String className)
    255    {
    256        return parent.getResource(PlasticInternalUtils.toClassPath(className)) != null;
    257    }
    258
    259    public PlasticProxyFactory getProxyFactory()
    260    {
    261        return proxyFactory;
    262    }
    263
    264    public void transform(final PlasticClass plasticClass)
    265    {
    266        tracker.run(String.format("Running component class transformations on %s", plasticClass.getClassName()),
    267                new Runnable()
    268                {
    269                    public void run()
    270                    {
    271                        String className = plasticClass.getClassName();
    272                        String parentClassName = plasticClass.getSuperClassName();
    273
    274                        // The parent model may not exist, if the super class is not in a controlled package.
    275
    276                        ComponentModel parentModel = classToModel.get(parentClassName);
    277
    278                        final boolean isRoot = parentModel == null;
    279
    280                        if (isRoot
    281                                && !(parentClassName.equals("java.lang.Object") || parentClassName
    282                                .equals("groovy.lang.GroovyObjectSupport")))
    283                        {
    284                            String suggestedPackageName = buildSuggestedPackageName(className);
    285
    286                            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));
    287                        }
    288
    289                        // Tapestry 5.2 was more sensitive that the parent class have a public no-args constructor.
    290                        // Plastic
    291                        // doesn't care, and we don't have the tools to dig that information out.
    292
    293                        Logger logger = loggerSource.getLogger(className);
    294
    295                        Resource baseResource = new ClasspathResource(parent, PlasticInternalUtils
    296                                .toClassPath(className));
    297
    298                        changeTracker.add(baseResource.toURL());
    299
    300                        if (isRoot)
    301                        {
    302                            implementComponentInterface(plasticClass);
    303                        }
    304
    305                        boolean isPage = resolver.isPage(className);
    306
    307                        boolean superClassImplementsPageLifecycle = plasticClass.isInterfaceImplemented(PageLifecycleListener.class);
    308
    309                        String libraryName = resolver.getLibraryNameForClass(className);
    310
    311                        final MutableComponentModel model = new MutableComponentModelImpl(className, logger, baseResource,
    312                                parentModel, isPage, libraryName);
    313
    314                        TransformationSupportImpl transformationSupport = new TransformationSupportImpl(plasticClass, isRoot, model);
    315
    316                        transformerChain.transform(plasticClass, transformationSupport, model);
    317
    318                        transformationSupport.commit();
    319
    320                        if (!superClassImplementsPageLifecycle && plasticClass.isInterfaceImplemented(PageLifecycleListener.class))
    321                        {
    322                            plasticClass.onConstruct(REGISTER_AS_PAGE_LIFECYCLE_LISTENER);
    323                        }
    324
    325                        classToModel.put(className, model);
    326                    }
    327                });
    328    }
    329
    330    private void implementComponentInterface(PlasticClass plasticClass)
    331    {
    332        plasticClass.introduceInterface(Component.class);
    333
    334        final PlasticField resourcesField = plasticClass.introduceField(InternalComponentResources.class,
    335                "internalComponentResources").injectFromInstanceContext();
    336
    337        plasticClass.introduceMethod(GET_COMPONENT_RESOURCES, new InstructionBuilderCallback()
    338        {
    339            public void doBuild(InstructionBuilder builder)
    340            {
    341                builder.loadThis().getField(resourcesField).returnResult();
    342            }
    343        });
    344    }
    345
    346    public <T> ClassInstantiator<T> configureInstantiator(String className, ClassInstantiator<T> instantiator)
    347    {
    348        return instantiator;
    349    }
    350
    351    private String buildSuggestedPackageName(String className)
    352    {
    353        for (String subpackage : InternalConstants.SUBPACKAGES)
    354        {
    355            String term = "." + subpackage + ".";
    356
    357            int pos = className.indexOf(term);
    358
    359            // Keep the leading '.' in the subpackage name and tack on "base".
    360
    361            if (pos > 0)
    362                return className.substring(0, pos + 1) + InternalConstants.BASE_SUBPACKAGE;
    363        }
    364
    365        // Is this even reachable? className should always be in a controlled package and so
    366        // some subpackage above should have matched.
    367
    368        return null;
    369    }
    370
    371    public void classWillLoad(PlasticClassEvent event)
    372    {
    373        Logger logger = loggerSource.getLogger("tapestry.transformer." + event.getPrimaryClassName());
    374
    375        if (logger.isDebugEnabled())
    376            logger.debug(event.getDissasembledBytecode());
    377    }
    378
    379    private class TransformationSupportImpl implements TransformationSupport
    380    {
    381        private final PlasticClass plasticClass;
    382
    383        private final boolean root;
    384
    385        private final MutableComponentModel model;
    386
    387        private final List<MethodAdvice> eventHandlerAdvice = CollectionFactory.newList();
    388
    389        public TransformationSupportImpl(PlasticClass plasticClass, boolean root, MutableComponentModel model)
    390        {
    391            this.plasticClass = plasticClass;
    392            this.root = root;
    393            this.model = model;
    394        }
    395
    396        /**
    397         * Commits any stored changes to the PlasticClass; this is used to defer adding advice to the dispatch method.
    398         */
    399        public void commit()
    400        {
    401            if (!eventHandlerAdvice.isEmpty())
    402            {
    403                PlasticMethod dispatchMethod = plasticClass.introduceMethod(TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION);
    404                for (MethodAdvice advice : eventHandlerAdvice)
    405                {
    406                    dispatchMethod.addAdvice(advice);
    407                }
    408            }
    409        }
    410
    411        public Class toClass(String typeName)
    412        {
    413            try
    414            {
    415                return PlasticInternalUtils.toClass(manager.getClassLoader(), typeName);
    416            } catch (ClassNotFoundException ex)
    417            {
    418                throw new RuntimeException(String.format(
    419                        "Unable to convert type '%s' to a Class: %s", typeName,
    420                        ExceptionUtils.toMessage(ex)), ex);
    421            }
    422        }
    423
    424        public boolean isRootTransformation()
    425        {
    426            return root;
    427        }
    428
    429        public void addEventHandler(final String eventType, final int minContextValues, final String operationDescription, final ComponentEventHandler handler)
    430        {
    431            assert InternalUtils.isNonBlank(eventType);
    432            assert minContextValues >= 0;
    433            assert handler != null;
    434
    435            model.addEventHandler(eventType);
    436
    437            MethodAdvice advice = new EventMethodAdvice(tracker, eventType, minContextValues, operationDescription, handler);
    438
    439            // The advice is added at the very end, after the logic provided by the OnEventWorker
    440
    441            eventHandlerAdvice.add(advice);
    442        }
    443    }
    444
    445    private static class EventMethodAdvice implements MethodAdvice
    446    {
    447        final OperationTracker tracker;
    448        final String eventType;
    449        final int minContextValues;
    450        final String operationDescription;
    451        final ComponentEventHandler handler;
    452
    453        public EventMethodAdvice(OperationTracker tracker, String eventType, int minContextValues, String operationDescription, ComponentEventHandler handler)
    454        {
    455            this.tracker = tracker;
    456            this.eventType = eventType;
    457            this.minContextValues = minContextValues;
    458            this.operationDescription = operationDescription;
    459            this.handler = handler;
    460        }
    461
    462        public void advise(final MethodInvocation invocation)
    463        {
    464            final ComponentEvent event = (ComponentEvent) invocation.getParameter(0);
    465
    466            boolean matches = !event.isAborted() && event.matches(eventType, "", minContextValues);
    467
    468            if (matches)
    469            {
    470                tracker.run(operationDescription, new Runnable()
    471                {
    472                    public void run()
    473                    {
    474                        Component instance = (Component) invocation.getInstance();
    475
    476                        handler.handleEvent(instance, event);
    477                    }
    478                });
    479            }
    480
    481            // Order of operations is key here. This logic takes precedence; base class event dispatch and event handler methods
    482            // in the class come AFTER.
    483
    484            invocation.proceed();
    485
    486            if (matches)
    487            {
    488                invocation.setReturnValue(true);
    489            }
    490        }
    491    }
    492}