001    // Copyright 2006, 2007, 2008, 2010, 2011 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    
015    package org.apache.tapestry5.internal.services;
016    
017    import org.apache.tapestry5.ComponentResources;
018    import org.apache.tapestry5.SymbolConstants;
019    import org.apache.tapestry5.internal.InternalComponentResources;
020    import org.apache.tapestry5.internal.InternalConstants;
021    import org.apache.tapestry5.internal.model.MutableComponentModelImpl;
022    import org.apache.tapestry5.internal.plastic.PlasticInternalUtils;
023    import org.apache.tapestry5.ioc.Invokable;
024    import org.apache.tapestry5.ioc.LoggerSource;
025    import org.apache.tapestry5.ioc.OperationTracker;
026    import org.apache.tapestry5.ioc.Resource;
027    import org.apache.tapestry5.ioc.annotations.PostInjection;
028    import org.apache.tapestry5.ioc.annotations.Primary;
029    import org.apache.tapestry5.ioc.annotations.Symbol;
030    import org.apache.tapestry5.ioc.internal.services.ClassFactoryImpl;
031    import org.apache.tapestry5.ioc.internal.services.PlasticProxyFactoryImpl;
032    import org.apache.tapestry5.ioc.internal.util.ClasspathResource;
033    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
034    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
035    import org.apache.tapestry5.ioc.internal.util.URLChangeTracker;
036    import org.apache.tapestry5.ioc.services.Builtin;
037    import org.apache.tapestry5.ioc.services.ClassFactory;
038    import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
039    import org.apache.tapestry5.ioc.services.PlasticProxyFactory;
040    import org.apache.tapestry5.model.ComponentModel;
041    import org.apache.tapestry5.model.MutableComponentModel;
042    import org.apache.tapestry5.plastic.*;
043    import org.apache.tapestry5.plastic.PlasticManager.PlasticManagerBuilder;
044    import org.apache.tapestry5.runtime.Component;
045    import org.apache.tapestry5.runtime.ComponentEvent;
046    import org.apache.tapestry5.runtime.ComponentResourcesAware;
047    import org.apache.tapestry5.runtime.PageLifecycleListener;
048    import org.apache.tapestry5.services.*;
049    import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
050    import org.apache.tapestry5.services.transform.ControlledPackageType;
051    import org.apache.tapestry5.services.transform.TransformationSupport;
052    import org.slf4j.Logger;
053    
054    import java.util.Map;
055    import java.util.Set;
056    
057    /**
058     * A wrapper around a {@link PlasticManager} that allows certain classes to be modified as they are loaded.
059     */
060    public 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    
178            // Release the existing class pool, loader and so forth.
179            // Create a new one.
180    
181            initializeService();
182        }
183    
184        /**
185         * Invoked at object creation, or when there are updates to class files (i.e., invalidation), to create a new set of
186         * Javassist class pools and loaders.
187         */
188        private void initializeService()
189        {
190            PlasticManagerBuilder builder = PlasticManager.withClassLoader(parent).delegate(this)
191                    .packages(controlledPackageNames);
192    
193            if (!productionMode)
194            {
195                builder.enable(TransformationOption.FIELD_WRITEBEHIND);
196            }
197    
198            manager = builder.create();
199    
200            manager.addPlasticClassListener(this);
201    
202            classFactory = new ClassFactoryImpl(manager.getClassLoader(), logger);
203    
204            proxyFactory = new PlasticProxyFactoryImpl(manager, logger);
205    
206            classToInstantiator.clear();
207            classToModel.clear();
208        }
209    
210        public synchronized Instantiator getInstantiator(final String className)
211        {
212            Instantiator result = classToInstantiator.get(className);
213    
214            if (result == null)
215            {
216                result = createInstantiatorForClass(className);
217    
218                classToInstantiator.put(className, result);
219            }
220    
221            return result;
222        }
223    
224        private Instantiator createInstantiatorForClass(final String className)
225        {
226            return tracker.invoke(String.format("Creating instantiator for component class %s", className),
227                    new Invokable<Instantiator>()
228                    {
229                        public Instantiator invoke()
230                        {
231                            // Force the creation of the class (and the transformation of the class). This will first
232                            // trigger transformations of any base classes.
233    
234                            final ClassInstantiator<Component> plasticInstantiator = manager.getClassInstantiator(className);
235    
236                            final ComponentModel model = classToModel.get(className);
237    
238                            return new Instantiator()
239                            {
240                                public Component newInstance(InternalComponentResources resources)
241                                {
242                                    return plasticInstantiator.with(ComponentResources.class, resources)
243                                            .with(InternalComponentResources.class, resources).newInstance();
244                                }
245    
246                                public ComponentModel getModel()
247                                {
248                                    return model;
249                                }
250    
251                                @Override
252                                public String toString()
253                                {
254                                    return String.format("[Instantiator[%s]", className);
255                                }
256                            };
257                        }
258                    });
259        }
260    
261        public boolean exists(String className)
262        {
263            return parent.getResource(PlasticInternalUtils.toClassPath(className)) != null;
264        }
265    
266        public ClassFactory getClassFactory()
267        {
268            return classFactory;
269        }
270    
271        public PlasticProxyFactory getProxyFactory()
272        {
273            return proxyFactory;
274        }
275    
276        public void transform(final PlasticClass plasticClass)
277        {
278            tracker.run(String.format("Running component class transformations on %s", plasticClass.getClassName()),
279                    new Runnable()
280                    {
281                        public void run()
282                        {
283                            String className = plasticClass.getClassName();
284                            String parentClassName = plasticClass.getSuperClassName();
285    
286                            // The parent model may not exist, if the super class is not in a controlled package.
287    
288                            ComponentModel parentModel = classToModel.get(parentClassName);
289    
290                            final boolean isRoot = parentModel == null;
291    
292                            if (isRoot
293                                    && !(parentClassName.equals("java.lang.Object") || parentClassName
294                                    .equals("groovy.lang.GroovyObjectSupport")))
295                            {
296                                String suggestedPackageName = buildSuggestedPackageName(className);
297    
298                                throw new RuntimeException(ServicesMessages.baseClassInWrongPackage(parentClassName,
299                                        className, suggestedPackageName));
300                            }
301    
302                            // Tapestry 5.2 was more sensitive that the parent class have a public no-args constructor.
303                            // Plastic
304                            // doesn't care, and we don't have the tools to dig that information out.
305    
306                            Logger logger = loggerSource.getLogger(className);
307    
308                            Resource baseResource = new ClasspathResource(parent, PlasticInternalUtils
309                                    .toClassPath(className));
310    
311                            changeTracker.add(baseResource.toURL());
312    
313                            if (isRoot)
314                            {
315                                implementComponentInterface(plasticClass);
316                            }
317    
318                            boolean isPage = resolver.isPage(className);
319    
320                            boolean superClassImplementsPageLifecycle = plasticClass.isInterfaceImplemented(PageLifecycleListener.class);
321    
322                            final MutableComponentModel model = new MutableComponentModelImpl(className, logger, baseResource,
323                                    parentModel, isPage);
324    
325                            transformerChain.transform(plasticClass, new TransformationSupportImpl(plasticClass, isRoot, model), model);
326    
327                            if (!superClassImplementsPageLifecycle && plasticClass.isInterfaceImplemented(PageLifecycleListener.class))
328                            {
329                                plasticClass.onConstruct(REGISTER_AS_PAGE_LIFECYCLE_LISTENER);
330                            }
331    
332                            classToModel.put(className, model);
333                        }
334                    });
335        }
336    
337        private void implementComponentInterface(PlasticClass plasticClass)
338        {
339            plasticClass.introduceInterface(Component.class);
340    
341            final PlasticField resourcesField = plasticClass.introduceField(InternalComponentResources.class,
342                    "internalComponentResources").injectFromInstanceContext();
343    
344            plasticClass.introduceMethod(GET_COMPONENT_RESOURCES, new InstructionBuilderCallback()
345            {
346                public void doBuild(InstructionBuilder builder)
347                {
348                    builder.loadThis().getField(resourcesField).returnResult();
349                }
350            });
351        }
352    
353        public <T> ClassInstantiator<T> configureInstantiator(String className, ClassInstantiator<T> instantiator)
354        {
355            return instantiator;
356        }
357    
358        private String buildSuggestedPackageName(String className)
359        {
360            for (String subpackage : InternalConstants.SUBPACKAGES)
361            {
362                String term = "." + subpackage + ".";
363    
364                int pos = className.indexOf(term);
365    
366                // Keep the leading '.' in the subpackage name and tack on "base".
367    
368                if (pos > 0)
369                    return className.substring(0, pos + 1) + InternalConstants.BASE_SUBPACKAGE;
370            }
371    
372            // Is this even reachable? className should always be in a controlled package and so
373            // some subpackage above should have matched.
374    
375            return null;
376        }
377    
378        public void classWillLoad(PlasticClassEvent event)
379        {
380            Logger logger = loggerSource.getLogger("tapestry.transformer." + event.getPrimaryClassName());
381    
382            if (logger.isDebugEnabled())
383                logger.debug(event.getDissasembledBytecode());
384        }
385    
386        private class TransformationSupportImpl implements TransformationSupport
387        {
388            private final PlasticClass plasticClass;
389    
390            private final boolean root;
391    
392            private final MutableComponentModel model;
393    
394            public TransformationSupportImpl(PlasticClass plasticClass, boolean root, MutableComponentModel model)
395            {
396                this.plasticClass = plasticClass;
397                this.root = root;
398                this.model = model;
399            }
400    
401            public Class toClass(String typeName)
402            {
403                try
404                {
405                    return PlasticInternalUtils.toClass(manager.getClassLoader(), typeName);
406                } catch (ClassNotFoundException ex)
407                {
408                    throw new RuntimeException(String.format(
409                            "Unable to convert type '%s' to a Class: %s", typeName,
410                            InternalUtils.toMessage(ex)), ex);
411                }
412            }
413    
414            public boolean isRootTransformation()
415            {
416                return root;
417            }
418    
419            public void addEventHandler(final String eventType, final int minContextValues, final String operationDescription, final ComponentEventHandler handler)
420            {
421                assert InternalUtils.isNonBlank(eventType);
422                assert minContextValues >= 0;
423                assert handler != null;
424    
425                model.addEventHandler(eventType);
426    
427                MethodAdvice advice = new MethodAdvice()
428                {
429                    public void advise(MethodInvocation invocation)
430                    {
431                        final ComponentEvent event = (ComponentEvent) invocation.getParameter(0);
432    
433                        boolean matches = !event.isAborted() && event.matches(eventType, "", minContextValues);
434    
435                        if (matches)
436                        {
437                            final Component instance = (Component) invocation.getInstance();
438    
439                            tracker.invoke(operationDescription, new Invokable<Object>()
440                            {
441                                public Object invoke()
442                                {
443                                    handler.handleEvent(instance, event);
444    
445                                    return null;
446                                }
447                            });
448                        }
449    
450                        // Order of operations is key here. This logic takes precedence; base class event dispatch and event handler methods
451                        // in the class come AFTER.
452    
453                        invocation.proceed();
454    
455                        if (matches)
456                        {
457                            invocation.setReturnValue(true);
458                        }
459                    }
460                };
461    
462                plasticClass.introduceMethod(TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION).addAdvice(advice);
463            }
464        }
465    }