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    
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            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    }