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