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}