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}