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}