001// Copyright 2006, 2007, 2008, 2010, 2011, 2012 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007// http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.internal.services;
016
017import org.apache.tapestry5.ComponentResources;
018import org.apache.tapestry5.SymbolConstants;
019import org.apache.tapestry5.internal.InternalComponentResources;
020import org.apache.tapestry5.internal.InternalConstants;
021import org.apache.tapestry5.internal.model.MutableComponentModelImpl;
022import org.apache.tapestry5.internal.plastic.PlasticInternalUtils;
023import org.apache.tapestry5.ioc.Invokable;
024import org.apache.tapestry5.ioc.LoggerSource;
025import org.apache.tapestry5.ioc.OperationTracker;
026import org.apache.tapestry5.ioc.Resource;
027import org.apache.tapestry5.ioc.annotations.PostInjection;
028import org.apache.tapestry5.ioc.annotations.Primary;
029import org.apache.tapestry5.ioc.annotations.Symbol;
030import org.apache.tapestry5.ioc.internal.services.ClassFactoryImpl;
031import org.apache.tapestry5.ioc.internal.services.PlasticProxyFactoryImpl;
032import org.apache.tapestry5.ioc.internal.util.ClasspathResource;
033import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
034import org.apache.tapestry5.ioc.internal.util.InternalUtils;
035import org.apache.tapestry5.ioc.internal.util.URLChangeTracker;
036import org.apache.tapestry5.ioc.services.Builtin;
037import org.apache.tapestry5.ioc.services.ClassFactory;
038import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
039import org.apache.tapestry5.ioc.services.PlasticProxyFactory;
040import org.apache.tapestry5.model.ComponentModel;
041import org.apache.tapestry5.model.MutableComponentModel;
042import org.apache.tapestry5.plastic.*;
043import org.apache.tapestry5.plastic.PlasticManager.PlasticManagerBuilder;
044import org.apache.tapestry5.runtime.Component;
045import org.apache.tapestry5.runtime.ComponentEvent;
046import org.apache.tapestry5.runtime.ComponentResourcesAware;
047import org.apache.tapestry5.runtime.PageLifecycleListener;
048import org.apache.tapestry5.services.*;
049import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
050import org.apache.tapestry5.services.transform.ControlledPackageType;
051import org.apache.tapestry5.services.transform.TransformationSupport;
052import org.slf4j.Logger;
053
054import java.util.Map;
055import java.util.Set;
056
057/**
058 * A wrapper around a {@link PlasticManager} that allows certain classes to be modified as they are loaded.
059 */
060public final class ComponentInstantiatorSourceImpl implements ComponentInstantiatorSource, UpdateListener,
061        InvalidationListener, PlasticManagerDelegate, PlasticClassListener
062{
063    private final Set<String> controlledPackageNames = CollectionFactory.newSet();
064
065    private final URLChangeTracker changeTracker;
066
067    private final ClassLoader parent;
068
069    private final ComponentClassTransformWorker2 transformerChain;
070
071    private final LoggerSource loggerSource;
072
073    private final Logger logger;
074
075    private final OperationTracker tracker;
076
077    private final InternalComponentInvalidationEventHub invalidationHub;
078
079    private final boolean productionMode;
080
081    private final ComponentClassResolver resolver;
082
083    // These change whenever the invalidation event hub sends an invalidation notification
084
085    private volatile ClassFactory classFactory;
086
087    private volatile PlasticProxyFactory proxyFactory;
088
089    private volatile PlasticManager manager;
090
091    /**
092     * Map from class name to Instantiator.
093     */
094    private final Map<String, Instantiator> classToInstantiator = CollectionFactory.newMap();
095
096    private final Map<String, ComponentModel> classToModel = CollectionFactory.newMap();
097
098    private final MethodDescription GET_COMPONENT_RESOURCES = PlasticUtils.getMethodDescription(
099            ComponentResourcesAware.class, "getComponentResources");
100
101    private final ConstructorCallback REGISTER_AS_PAGE_LIFECYCLE_LISTENER = new ConstructorCallback()
102    {
103        public void onConstruct(Object instance, InstanceContext context)
104        {
105            InternalComponentResources resources = context.get(InternalComponentResources.class);
106
107            resources.addPageLifecycleListener((PageLifecycleListener) instance);
108        }
109    };
110
111    public ComponentInstantiatorSourceImpl(Logger logger,
112
113                                           LoggerSource loggerSource,
114
115                                           @Builtin
116                                           PlasticProxyFactory proxyFactory,
117
118                                           @Primary
119                                           ComponentClassTransformWorker2 transformerChain,
120
121                                           ClasspathURLConverter classpathURLConverter,
122
123                                           OperationTracker tracker,
124
125                                           Map<String, ControlledPackageType> configuration,
126
127                                           @Symbol(SymbolConstants.PRODUCTION_MODE)
128                                           boolean productionMode,
129
130                                           ComponentClassResolver resolver,
131
132                                           InternalComponentInvalidationEventHub invalidationHub)
133    {
134        this.parent = proxyFactory.getClassLoader();
135        this.transformerChain = transformerChain;
136        this.logger = logger;
137        this.loggerSource = loggerSource;
138        this.changeTracker = new URLChangeTracker(classpathURLConverter);
139        this.tracker = tracker;
140        this.invalidationHub = invalidationHub;
141        this.productionMode = productionMode;
142        this.resolver = resolver;
143
144        // For now, we just need the keys of the configuration. When there are more types of controlled
145        // packages, we'll need to do more.
146
147        controlledPackageNames.addAll(configuration.keySet());
148
149        initializeService();
150    }
151
152    @PostInjection
153    public void listenForUpdates(UpdateListenerHub hub)
154    {
155        invalidationHub.addInvalidationListener(this);
156        hub.addUpdateListener(this);
157    }
158
159    public synchronized void checkForUpdates()
160    {
161        if (changeTracker.containsChanges())
162        {
163            invalidationHub.classInControlledPackageHasChanged();
164        }
165    }
166
167    public void forceComponentInvalidation()
168    {
169        changeTracker.clear();
170        invalidationHub.classInControlledPackageHasChanged();
171    }
172
173    public void objectWasInvalidated()
174    {
175        changeTracker.clear();
176        classToInstantiator.clear();
177        proxyFactory.clearCache();
178
179        // Release the existing class pool, loader and so forth.
180        // Create a new one.
181
182        initializeService();
183    }
184
185    /**
186     * Invoked at object creation, or when there are updates to class files (i.e., invalidation), to create a new set of
187     * Javassist class pools and loaders.
188     */
189    private void initializeService()
190    {
191        PlasticManagerBuilder builder = PlasticManager.withClassLoader(parent).delegate(this)
192                .packages(controlledPackageNames);
193
194        if (!productionMode)
195        {
196            builder.enable(TransformationOption.FIELD_WRITEBEHIND);
197        }
198
199        manager = builder.create();
200
201        manager.addPlasticClassListener(this);
202
203        classFactory = new ClassFactoryImpl(manager.getClassLoader(), logger);
204
205        proxyFactory = new PlasticProxyFactoryImpl(manager, logger);
206
207        classToInstantiator.clear();
208        classToModel.clear();
209    }
210
211    public synchronized Instantiator getInstantiator(final String className)
212    {
213        Instantiator result = classToInstantiator.get(className);
214
215        if (result == null)
216        {
217            result = createInstantiatorForClass(className);
218
219            classToInstantiator.put(className, result);
220        }
221
222        return result;
223    }
224
225    private Instantiator createInstantiatorForClass(final String className)
226    {
227        return tracker.invoke(String.format("Creating instantiator for component class %s", className),
228                new Invokable<Instantiator>()
229                {
230                    public Instantiator invoke()
231                    {
232                        // Force the creation of the class (and the transformation of the class). This will first
233                        // trigger transformations of any base classes.
234
235                        final ClassInstantiator<Component> plasticInstantiator = manager.getClassInstantiator(className);
236
237                        final ComponentModel model = classToModel.get(className);
238
239                        return new Instantiator()
240                        {
241                            public Component newInstance(InternalComponentResources resources)
242                            {
243                                return plasticInstantiator.with(ComponentResources.class, resources)
244                                        .with(InternalComponentResources.class, resources).newInstance();
245                            }
246
247                            public ComponentModel getModel()
248                            {
249                                return model;
250                            }
251
252                            @Override
253                            public String toString()
254                            {
255                                return String.format("[Instantiator[%s]", className);
256                            }
257                        };
258                    }
259                });
260    }
261
262    public boolean exists(String className)
263    {
264        return parent.getResource(PlasticInternalUtils.toClassPath(className)) != null;
265    }
266
267    public ClassFactory getClassFactory()
268    {
269        return classFactory;
270    }
271
272    public PlasticProxyFactory getProxyFactory()
273    {
274        return proxyFactory;
275    }
276
277    public void transform(final PlasticClass plasticClass)
278    {
279        tracker.run(String.format("Running component class transformations on %s", plasticClass.getClassName()),
280                new Runnable()
281                {
282                    public void run()
283                    {
284                        String className = plasticClass.getClassName();
285                        String parentClassName = plasticClass.getSuperClassName();
286
287                        // The parent model may not exist, if the super class is not in a controlled package.
288
289                        ComponentModel parentModel = classToModel.get(parentClassName);
290
291                        final boolean isRoot = parentModel == null;
292
293                        if (isRoot
294                                && !(parentClassName.equals("java.lang.Object") || parentClassName
295                                .equals("groovy.lang.GroovyObjectSupport")))
296                        {
297                            String suggestedPackageName = buildSuggestedPackageName(className);
298
299                            throw new RuntimeException(ServicesMessages.baseClassInWrongPackage(parentClassName,
300                                    className, suggestedPackageName));
301                        }
302
303                        // Tapestry 5.2 was more sensitive that the parent class have a public no-args constructor.
304                        // Plastic
305                        // doesn't care, and we don't have the tools to dig that information out.
306
307                        Logger logger = loggerSource.getLogger(className);
308
309                        Resource baseResource = new ClasspathResource(parent, PlasticInternalUtils
310                                .toClassPath(className));
311
312                        changeTracker.add(baseResource.toURL());
313
314                        if (isRoot)
315                        {
316                            implementComponentInterface(plasticClass);
317                        }
318
319                        boolean isPage = resolver.isPage(className);
320
321                        boolean superClassImplementsPageLifecycle = plasticClass.isInterfaceImplemented(PageLifecycleListener.class);
322
323                        final MutableComponentModel model = new MutableComponentModelImpl(className, logger, baseResource,
324                                parentModel, isPage);
325
326                        transformerChain.transform(plasticClass, new TransformationSupportImpl(plasticClass, isRoot, model), model);
327
328                        if (!superClassImplementsPageLifecycle && plasticClass.isInterfaceImplemented(PageLifecycleListener.class))
329                        {
330                            plasticClass.onConstruct(REGISTER_AS_PAGE_LIFECYCLE_LISTENER);
331                        }
332
333                        classToModel.put(className, model);
334                    }
335                });
336    }
337
338    private void implementComponentInterface(PlasticClass plasticClass)
339    {
340        plasticClass.introduceInterface(Component.class);
341
342        final PlasticField resourcesField = plasticClass.introduceField(InternalComponentResources.class,
343                "internalComponentResources").injectFromInstanceContext();
344
345        plasticClass.introduceMethod(GET_COMPONENT_RESOURCES, new InstructionBuilderCallback()
346        {
347            public void doBuild(InstructionBuilder builder)
348            {
349                builder.loadThis().getField(resourcesField).returnResult();
350            }
351        });
352    }
353
354    public <T> ClassInstantiator<T> configureInstantiator(String className, ClassInstantiator<T> instantiator)
355    {
356        return instantiator;
357    }
358
359    private String buildSuggestedPackageName(String className)
360    {
361        for (String subpackage : InternalConstants.SUBPACKAGES)
362        {
363            String term = "." + subpackage + ".";
364
365            int pos = className.indexOf(term);
366
367            // Keep the leading '.' in the subpackage name and tack on "base".
368
369            if (pos > 0)
370                return className.substring(0, pos + 1) + InternalConstants.BASE_SUBPACKAGE;
371        }
372
373        // Is this even reachable? className should always be in a controlled package and so
374        // some subpackage above should have matched.
375
376        return null;
377    }
378
379    public void classWillLoad(PlasticClassEvent event)
380    {
381        Logger logger = loggerSource.getLogger("tapestry.transformer." + event.getPrimaryClassName());
382
383        if (logger.isDebugEnabled())
384            logger.debug(event.getDissasembledBytecode());
385    }
386
387    private class TransformationSupportImpl implements TransformationSupport
388    {
389        private final PlasticClass plasticClass;
390
391        private final boolean root;
392
393        private final MutableComponentModel model;
394
395        public TransformationSupportImpl(PlasticClass plasticClass, boolean root, MutableComponentModel model)
396        {
397            this.plasticClass = plasticClass;
398            this.root = root;
399            this.model = model;
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                        InternalUtils.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            plasticClass.introduceMethod(TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION).addAdvice(advice);
431        }
432    }
433    
434    private static class EventMethodAdvice implements MethodAdvice
435    {
436        final OperationTracker tracker;
437        final String eventType;
438        final int minContextValues;
439        final String operationDescription;
440        final ComponentEventHandler handler;
441
442        public EventMethodAdvice(OperationTracker tracker, String eventType, int minContextValues, String operationDescription, ComponentEventHandler handler)
443        {
444            this.tracker = tracker;
445            this.eventType = eventType;
446            this.minContextValues = minContextValues;
447            this.operationDescription = operationDescription;
448            this.handler = handler;
449        }
450        
451        public void advise(MethodInvocation invocation)
452        {
453            final ComponentEvent event = (ComponentEvent) invocation.getParameter(0);
454
455            boolean matches = !event.isAborted() && event.matches(eventType, "", minContextValues);
456
457            if (matches)
458            {
459                final Component instance = (Component) invocation.getInstance();
460
461                tracker.invoke(operationDescription, new Invokable<Object>()
462                {
463                    public Object invoke()
464                    {
465                        handler.handleEvent(instance, event);
466
467                        return null;
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    
484}